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 }