mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Services/HomeServicePropertyTests.cs
2026-02-09 14:45:06 +08:00

594 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// HomeService 属性测试
/// 验证首页服务的列表查询过滤和排序正确性
/// </summary>
public class HomeServicePropertyTests
{
private readonly Mock<ILogger<HomeService>> _mockLogger = new();
#region Property 1: - Banner
/// <summary>
/// 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**
/// </summary>
[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();
}
/// <summary>
/// Property 1: Banner列表不返回已删除的记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1**
/// </summary>
[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
/// <summary>
/// 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**
/// </summary>
[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();
}
/// <summary>
/// Property 1: 测评类型列表包含所有状态的未删除记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.2**
/// </summary>
[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
/// <summary>
/// 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**
/// </summary>
[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();
}
/// <summary>
/// Property 1: 宣传图列表不返回团队页位置的记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.3**
/// </summary>
[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
/// <summary>
/// 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**
/// </summary>
[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<int>();
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;
}
/// <summary>
/// Property 2: Banner列表排序稳定性
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1**
/// </summary>
[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
/// <summary>
/// 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**
/// </summary>
[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
/// <summary>
/// Property 1: 空数据库返回空列表
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1, 1.2, 1.3**
/// </summary>
[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);
}
/// <summary>
/// Property 1: 所有记录都被过滤时返回空列表
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 1.1, 1.2, 1.3**
/// </summary>
[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
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private MiAssessmentDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new MiAssessmentDbContext(options);
}
/// <summary>
/// 创建测试Banner
/// </summary>
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
};
}
/// <summary>
/// 创建指定Sort值的测试Banner
/// </summary>
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
};
}
/// <summary>
/// 创建测试AssessmentType
/// </summary>
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
};
}
/// <summary>
/// 创建测试Promotion
/// </summary>
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
};
}
/// <summary>
/// 创建指定Sort值的测试Promotion
/// </summary>
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
}