using FsCheck;
using FsCheck.Xunit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MiAssessment.Core.Services;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Services;
///
/// HomeService 属性测试
/// 验证首页服务的列表查询过滤和排序正确性
///
public class HomeServicePropertyTests
{
private readonly Mock> _mockLogger = new();
#region Property 1: 列表查询过滤正确性 - Banner
///
/// Property 1: Banner列表只返回启用状态的记录
/// *For any* Banner list query, the returned items SHALL only include records
/// with Status=1 and IsDeleted=false.
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1**
///
[Property(MaxTest = 100)]
public bool BannerListOnlyReturnsEnabledRecords(PositiveInt seed)
{
// Arrange: 创建包含不同状态的Banner数据
using var dbContext = CreateDbContext();
var random = new Random(seed.Get);
// 创建启用的Banner
for (int i = 0; i < 3; i++)
{
dbContext.Banners.Add(CreateBanner(seed.Get + i, status: 1, isDeleted: false));
}
// 创建禁用的Banner
for (int i = 0; i < 2; i++)
{
dbContext.Banners.Add(CreateBanner(seed.Get + 100 + i, status: 0, isDeleted: false));
}
// 创建已删除的Banner
for (int i = 0; i < 2; i++)
{
dbContext.Banners.Add(CreateBanner(seed.Get + 200 + i, status: 1, isDeleted: true));
}
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act: 调用服务方法
var result = service.GetBannerListAsync().GetAwaiter().GetResult();
// Assert: 验证返回结果只包含Status=1且未删除的记录
// 1. 返回的记录数应该等于启用且未删除的记录数
if (result.Count != 3) return false;
// 2. 验证数据库中确实存在被过滤掉的记录
var allBanners = dbContext.Banners.ToList();
var disabledBanners = allBanners.Where(b => b.Status == 0 && !b.IsDeleted).ToList();
var deletedBanners = allBanners.Where(b => b.IsDeleted).ToList();
if (disabledBanners.Count != 2 || deletedBanners.Count != 2) return false;
// 3. 验证返回的记录中不包含禁用或已删除的记录
var returnedIds = result.Select(b => b.Id).ToHashSet();
var disabledIds = disabledBanners.Select(b => b.Id).ToHashSet();
var deletedIds = deletedBanners.Select(b => b.Id).ToHashSet();
return !returnedIds.Intersect(disabledIds).Any() && !returnedIds.Intersect(deletedIds).Any();
}
///
/// Property 1: Banner列表不返回已删除的记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1**
///
[Property(MaxTest = 100)]
public bool BannerListExcludesDeletedRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建一些启用的Banner
var activeBanner = CreateBanner(seed.Get, status: 1, isDeleted: false);
dbContext.Banners.Add(activeBanner);
// 创建已删除的Banner(即使Status=1)
var deletedBanner = CreateBanner(seed.Get + 1, status: 1, isDeleted: true);
dbContext.Banners.Add(deletedBanner);
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var result = service.GetBannerListAsync().GetAwaiter().GetResult();
// Assert: 已删除的记录不应出现在结果中
return result.Count == 1 && result[0].Id == activeBanner.Id;
}
#endregion
#region Property 1: 列表查询过滤正确性 - AssessmentType
///
/// Property 1: 测评类型列表只返回未删除的记录
/// *For any* AssessmentType list query, the returned items SHALL only include
/// records with IsDeleted=false.
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.2**
///
[Property(MaxTest = 100)]
public bool AssessmentTypeListOnlyReturnsNonDeletedRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建未删除的测评类型(包含不同状态)
for (int i = 0; i < 3; i++)
{
dbContext.AssessmentTypes.Add(CreateAssessmentType(seed.Get + i, status: i, isDeleted: false));
}
// 创建已删除的测评类型
for (int i = 0; i < 2; i++)
{
dbContext.AssessmentTypes.Add(CreateAssessmentType(seed.Get + 100 + i, status: 1, isDeleted: true));
}
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var result = service.GetAssessmentListAsync().GetAwaiter().GetResult();
// Assert: 返回的记录数应该等于未删除的记录数
if (result.Count != 3) return false;
// 验证返回的记录中不包含已删除的记录
var allTypes = dbContext.AssessmentTypes.ToList();
var deletedTypes = allTypes.Where(a => a.IsDeleted).ToList();
var returnedIds = result.Select(a => a.Id).ToHashSet();
var deletedIds = deletedTypes.Select(a => a.Id).ToHashSet();
return !returnedIds.Intersect(deletedIds).Any();
}
///
/// Property 1: 测评类型列表包含所有状态的未删除记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.2**
///
[Property(MaxTest = 100)]
public bool AssessmentTypeListIncludesAllStatusRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建不同状态的测评类型:0已下线 1已上线 2即将上线
var offlineType = CreateAssessmentType(seed.Get, status: 0, isDeleted: false);
var onlineType = CreateAssessmentType(seed.Get + 1, status: 1, isDeleted: false);
var comingSoonType = CreateAssessmentType(seed.Get + 2, status: 2, isDeleted: false);
dbContext.AssessmentTypes.AddRange(offlineType, onlineType, comingSoonType);
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var result = service.GetAssessmentListAsync().GetAwaiter().GetResult();
// Assert: 应该返回所有未删除的记录,不管状态如何
var returnedIds = result.Select(a => a.Id).ToHashSet();
return returnedIds.Contains(offlineType.Id)
&& returnedIds.Contains(onlineType.Id)
&& returnedIds.Contains(comingSoonType.Id);
}
#endregion
#region Property 1: 列表查询过滤正确性 - Promotion
///
/// Property 1: 宣传图列表只返回首页位置且启用状态的记录
/// *For any* Promotion list query, the returned items SHALL only include records
/// with Position=1, Status=1, and IsDeleted=false.
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.3**
///
[Property(MaxTest = 100)]
public bool PromotionListOnlyReturnsHomePageEnabledRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建首页位置且启用的宣传图
for (int i = 0; i < 2; i++)
{
dbContext.Promotions.Add(CreatePromotion(seed.Get + i, position: 1, status: 1, isDeleted: false));
}
// 创建团队页位置的宣传图(Position=2)
for (int i = 0; i < 2; i++)
{
dbContext.Promotions.Add(CreatePromotion(seed.Get + 100 + i, position: 2, status: 1, isDeleted: false));
}
// 创建首页位置但禁用的宣传图
dbContext.Promotions.Add(CreatePromotion(seed.Get + 200, position: 1, status: 0, isDeleted: false));
// 创建首页位置但已删除的宣传图
dbContext.Promotions.Add(CreatePromotion(seed.Get + 300, position: 1, status: 1, isDeleted: true));
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var result = service.GetPromotionListAsync().GetAwaiter().GetResult();
// Assert: 返回的记录数应该等于首页位置且启用且未删除的记录数
if (result.Count != 2) return false;
// 验证返回的记录中不包含其他位置、禁用或已删除的记录
var allPromotions = dbContext.Promotions.ToList();
var excludedPromotions = allPromotions.Where(p => p.Position != 1 || p.Status != 1 || p.IsDeleted).ToList();
var returnedIds = result.Select(p => p.Id).ToHashSet();
var excludedIds = excludedPromotions.Select(p => p.Id).ToHashSet();
return !returnedIds.Intersect(excludedIds).Any();
}
///
/// Property 1: 宣传图列表不返回团队页位置的记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.3**
///
[Property(MaxTest = 100)]
public bool PromotionListExcludesTeamPageRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建首页位置的宣传图
var homePromotion = CreatePromotion(seed.Get, position: 1, status: 1, isDeleted: false);
dbContext.Promotions.Add(homePromotion);
// 创建团队页位置的宣传图
var teamPromotion = CreatePromotion(seed.Get + 1, position: 2, status: 1, isDeleted: false);
dbContext.Promotions.Add(teamPromotion);
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var result = service.GetPromotionListAsync().GetAwaiter().GetResult();
// Assert: 只返回首页位置的记录
return result.Count == 1 && result[0].Id == homePromotion.Id;
}
#endregion
#region Property 2: 列表查询排序正确性 - Banner
///
/// Property 2: Banner列表按Sort字段降序排列
/// *For any* Banner list query, the returned items SHALL be ordered by Sort
/// field in descending order.
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1**
///
[Property(MaxTest = 100)]
public bool BannerListSortedBySortDescending(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var random = new Random(seed.Get);
// 创建具有不同Sort值的Banner
var sortValues = new List();
for (int i = 0; i < 5; i++)
{
var sortValue = random.Next(1, 1000);
sortValues.Add(sortValue);
dbContext.Banners.Add(CreateBannerWithSort(seed.Get + i, sortValue));
}
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var result = service.GetBannerListAsync().GetAwaiter().GetResult();
// Assert: 验证列表按Sort降序排列
if (result.Count < 2) return true;
for (int i = 0; i < result.Count - 1; i++)
{
// 获取当前和下一个Banner的Sort值
var currentBanner = dbContext.Banners.First(b => b.Id == result[i].Id);
var nextBanner = dbContext.Banners.First(b => b.Id == result[i + 1].Id);
if (currentBanner.Sort < nextBanner.Sort)
{
return false;
}
}
return true;
}
///
/// Property 2: Banner列表排序稳定性
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1**
///
[Property(MaxTest = 50)]
public bool BannerListSortingIsStable(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建具有相同Sort值的Banner
var sameSort = seed.Get % 100;
for (int i = 0; i < 3; i++)
{
var banner = CreateBannerWithSort(seed.Get + i, sameSort);
dbContext.Banners.Add(banner);
}
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act: 多次调用应该返回相同的顺序
var result1 = service.GetBannerListAsync().GetAwaiter().GetResult();
var result2 = service.GetBannerListAsync().GetAwaiter().GetResult();
// Assert: 两次调用返回的顺序应该相同
if (result1.Count != result2.Count) return false;
for (int i = 0; i < result1.Count; i++)
{
if (result1[i].Id != result2[i].Id) return false;
}
return true;
}
#endregion
#region Property 2: 列表查询排序正确性 - Promotion
///
/// Property 2: 宣传图列表按Sort字段降序排列
/// *For any* Promotion list query, the returned items SHALL be ordered by Sort
/// field in descending order.
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.3**
///
[Property(MaxTest = 100)]
public bool PromotionListSortedBySortDescending(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var random = new Random(seed.Get);
// 创建具有不同Sort值的宣传图
for (int i = 0; i < 5; i++)
{
var sortValue = random.Next(1, 1000);
dbContext.Promotions.Add(CreatePromotionWithSort(seed.Get + i, sortValue));
}
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var result = service.GetPromotionListAsync().GetAwaiter().GetResult();
// Assert: 验证列表按Sort降序排列
if (result.Count < 2) return true;
for (int i = 0; i < result.Count - 1; i++)
{
var currentPromotion = dbContext.Promotions.First(p => p.Id == result[i].Id);
var nextPromotion = dbContext.Promotions.First(p => p.Id == result[i + 1].Id);
if (currentPromotion.Sort < nextPromotion.Sort)
{
return false;
}
}
return true;
}
#endregion
#region 综合属性测试
///
/// Property 1: 空数据库返回空列表
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1, 1.2, 1.3**
///
[Fact]
public void EmptyDatabaseReturnsEmptyLists()
{
// Arrange
using var dbContext = CreateDbContext();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var banners = service.GetBannerListAsync().GetAwaiter().GetResult();
var assessmentTypes = service.GetAssessmentListAsync().GetAwaiter().GetResult();
var promotions = service.GetPromotionListAsync().GetAwaiter().GetResult();
// Assert
Assert.Empty(banners);
Assert.Empty(assessmentTypes);
Assert.Empty(promotions);
}
///
/// Property 1: 所有记录都被过滤时返回空列表
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1, 1.2, 1.3**
///
[Property(MaxTest = 50)]
public bool AllFilteredRecordsReturnsEmptyList(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 只创建不符合条件的记录
// Banner: 禁用或已删除
dbContext.Banners.Add(CreateBanner(seed.Get, status: 0, isDeleted: false));
dbContext.Banners.Add(CreateBanner(seed.Get + 1, status: 1, isDeleted: true));
// AssessmentType: 已删除
dbContext.AssessmentTypes.Add(CreateAssessmentType(seed.Get, status: 1, isDeleted: true));
// Promotion: 非首页位置、禁用或已删除
dbContext.Promotions.Add(CreatePromotion(seed.Get, position: 2, status: 1, isDeleted: false));
dbContext.Promotions.Add(CreatePromotion(seed.Get + 1, position: 1, status: 0, isDeleted: false));
dbContext.Promotions.Add(CreatePromotion(seed.Get + 2, position: 1, status: 1, isDeleted: true));
dbContext.SaveChanges();
var service = new HomeService(dbContext, _mockLogger.Object);
// Act
var banners = service.GetBannerListAsync().GetAwaiter().GetResult();
var assessmentTypes = service.GetAssessmentListAsync().GetAwaiter().GetResult();
var promotions = service.GetPromotionListAsync().GetAwaiter().GetResult();
// Assert: 所有列表都应该为空
return banners.Count == 0 && assessmentTypes.Count == 0 && promotions.Count == 0;
}
#endregion
#region 辅助方法
///
/// 创建内存数据库上下文
///
private MiAssessmentDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new MiAssessmentDbContext(options);
}
///
/// 创建测试Banner
///
private Banner CreateBanner(int seed, int status, bool isDeleted)
{
return new Banner
{
Title = $"Test Banner {seed}",
ImageUrl = $"https://example.com/banner_{seed}.jpg",
LinkType = 0,
Sort = seed % 100,
Status = status,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = isDeleted
};
}
///
/// 创建指定Sort值的测试Banner
///
private Banner CreateBannerWithSort(int seed, int sort)
{
return new Banner
{
Title = $"Test Banner {seed}",
ImageUrl = $"https://example.com/banner_{seed}.jpg",
LinkType = 0,
Sort = sort,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
}
///
/// 创建测试AssessmentType
///
private AssessmentType CreateAssessmentType(int seed, int status, bool isDeleted)
{
return new AssessmentType
{
Name = $"Test Assessment {seed}",
Code = $"TEST_{seed}",
ImageUrl = $"https://example.com/assessment_{seed}.jpg",
Price = 99.00m,
Sort = seed % 100,
Status = status,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = isDeleted
};
}
///
/// 创建测试Promotion
///
private Promotion CreatePromotion(int seed, int position, int status, bool isDeleted)
{
return new Promotion
{
Title = $"Test Promotion {seed}",
ImageUrl = $"https://example.com/promotion_{seed}.jpg",
Position = position,
Sort = seed % 100,
Status = status,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = isDeleted
};
}
///
/// 创建指定Sort值的测试Promotion
///
private Promotion CreatePromotionWithSort(int seed, int sort)
{
return new Promotion
{
Title = $"Test Promotion {seed}",
ImageUrl = $"https://example.com/promotion_{seed}.jpg",
Position = 1,
Sort = sort,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
}
#endregion
}