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

486 lines
16 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>
/// TeamService 属性测试
/// 验证团队服务的列表查询过滤和排序正确性
/// </summary>
public class TeamServicePropertyTests
{
private readonly Mock<ILogger<TeamService>> _mockLogger = new();
#region Property 1:
/// <summary>
/// Property 1: 团队介绍只返回Position=2的宣传图
/// *For any* Team info query, the returned items SHALL only include records
/// with Position=2, Status=1, and IsDeleted=false.
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 100)]
public bool TeamInfoOnlyReturnsTeamPagePromotions(PositiveInt seed)
{
// Arrange: 创建包含不同Position的Promotion数据
using var dbContext = CreateDbContext();
// 创建团队页位置Position=2且启用的宣传图
for (int i = 0; i < 3; i++)
{
dbContext.Promotions.Add(CreatePromotion(seed.Get + i, position: 2, status: 1, isDeleted: false));
}
// 创建首页位置Position=1的宣传图
for (int i = 0; i < 2; i++)
{
dbContext.Promotions.Add(CreatePromotion(seed.Get + 100 + i, position: 1, status: 1, isDeleted: false));
}
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act: 调用服务方法
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 验证返回结果只包含Position=2的记录
// 1. 返回的图片数应该等于团队页位置且启用且未删除的记录数
if (result.Images.Count != 3) return false;
// 2. 验证数据库中确实存在首页位置的记录
var allPromotions = dbContext.Promotions.ToList();
var homePagePromotions = allPromotions.Where(p => p.Position == 1 && p.Status == 1 && !p.IsDeleted).ToList();
if (homePagePromotions.Count != 2) return false;
// 3. 验证返回的图片URL都来自Position=2的记录
var teamPageImageUrls = allPromotions
.Where(p => p.Position == 2 && p.Status == 1 && !p.IsDeleted)
.Select(p => p.ImageUrl)
.ToHashSet();
return result.Images.All(img => teamPageImageUrls.Contains(img));
}
/// <summary>
/// Property 1: 团队介绍只返回启用状态的记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 100)]
public bool TeamInfoOnlyReturnsEnabledRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建团队页位置且启用的宣传图
var enabledPromotion = CreatePromotion(seed.Get, position: 2, status: 1, isDeleted: false);
dbContext.Promotions.Add(enabledPromotion);
// 创建团队页位置但禁用的宣传图
var disabledPromotion = CreatePromotion(seed.Get + 1, position: 2, status: 0, isDeleted: false);
dbContext.Promotions.Add(disabledPromotion);
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 禁用的记录不应出现在结果中
return result.Images.Count == 1 && result.Images[0] == enabledPromotion.ImageUrl;
}
/// <summary>
/// Property 1: 团队介绍不返回已删除的记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 100)]
public bool TeamInfoExcludesDeletedRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建团队页位置且启用的宣传图
var activePromotion = CreatePromotion(seed.Get, position: 2, status: 1, isDeleted: false);
dbContext.Promotions.Add(activePromotion);
// 创建团队页位置但已删除的宣传图即使Status=1
var deletedPromotion = CreatePromotion(seed.Get + 1, position: 2, status: 1, isDeleted: true);
dbContext.Promotions.Add(deletedPromotion);
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 已删除的记录不应出现在结果中
return result.Images.Count == 1 && result.Images[0] == activePromotion.ImageUrl;
}
/// <summary>
/// Property 1: 团队介绍不返回首页位置的记录
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 100)]
public bool TeamInfoExcludesHomePageRecords(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建团队页位置的宣传图
var teamPromotion = CreatePromotion(seed.Get, position: 2, status: 1, isDeleted: false);
dbContext.Promotions.Add(teamPromotion);
// 创建首页位置的宣传图
var homePromotion = CreatePromotion(seed.Get + 1, position: 1, status: 1, isDeleted: false);
dbContext.Promotions.Add(homePromotion);
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 只返回团队页位置的记录
return result.Images.Count == 1 && result.Images[0] == teamPromotion.ImageUrl;
}
/// <summary>
/// Property 1: 团队介绍按Sort字段降序排列
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 100)]
public bool TeamInfoSortedBySortDescending(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var random = new Random(seed.Get);
// 创建具有不同Sort值的团队页宣传图
var promotions = new List<Promotion>();
for (int i = 0; i < 5; i++)
{
var sortValue = random.Next(1, 1000);
var promotion = CreatePromotionWithSort(seed.Get + i, sortValue);
promotions.Add(promotion);
dbContext.Promotions.Add(promotion);
}
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 验证列表按Sort降序排列
if (result.Images.Count < 2) return true;
// 获取按Sort降序排列的预期顺序
var expectedOrder = promotions
.OrderByDescending(p => p.Sort)
.Select(p => p.ImageUrl)
.ToList();
for (int i = 0; i < result.Images.Count; i++)
{
if (result.Images[i] != expectedOrder[i])
{
return false;
}
}
return true;
}
/// <summary>
/// Property 1: 团队介绍第一个元素应该是Sort值最大的
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 100)]
public bool TeamInfoFirstItemHasHighestSort(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var random = new Random(seed.Get);
// 创建具有不同Sort值的团队页宣传图
var promotions = new List<Promotion>();
for (int i = 0; i < 5; i++)
{
var sortValue = random.Next(1, 1000);
var promotion = CreatePromotionWithSort(seed.Get + i, sortValue);
promotions.Add(promotion);
dbContext.Promotions.Add(promotion);
}
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 第一个元素应该是Sort值最大的
if (result.Images.Count == 0) return true;
var maxSortPromotion = promotions.OrderByDescending(p => p.Sort).First();
return result.Images[0] == maxSortPromotion.ImageUrl;
}
#endregion
#region
/// <summary>
/// Property 1: 空数据库返回空列表
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Fact]
public void EmptyDatabaseReturnsEmptyImageList()
{
// Arrange
using var dbContext = CreateDbContext();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert
Assert.Empty(result.Images);
}
/// <summary>
/// Property 1: 所有记录都被过滤时返回空列表
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 50)]
public bool AllFilteredRecordsReturnsEmptyList(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 只创建不符合条件的记录
// 首页位置的宣传图Position=1
dbContext.Promotions.Add(CreatePromotion(seed.Get, position: 1, status: 1, isDeleted: false));
// 团队页位置但禁用的宣传图
dbContext.Promotions.Add(CreatePromotion(seed.Get + 1, position: 2, status: 0, isDeleted: false));
// 团队页位置但已删除的宣传图
dbContext.Promotions.Add(CreatePromotion(seed.Get + 2, position: 2, status: 1, isDeleted: true));
// 禁用且已删除的宣传图
dbContext.Promotions.Add(CreatePromotion(seed.Get + 3, position: 2, status: 0, isDeleted: true));
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 列表应该为空
return result.Images.Count == 0;
}
/// <summary>
/// Property 1: 过滤和排序同时正确
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 100)]
public bool FilteringAndSortingWorkTogether(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var random = new Random(seed.Get);
// 创建团队页位置且启用的宣传图不同Sort值
var teamPromotions = new List<Promotion>();
for (int i = 0; i < 3; i++)
{
var sortValue = random.Next(1, 1000);
var promotion = CreatePromotionWithSort(seed.Get + i, sortValue);
teamPromotions.Add(promotion);
dbContext.Promotions.Add(promotion);
}
// 创建首页位置的宣传图高Sort值不应出现在结果中
var homePromotion = CreatePromotion(seed.Get + 100, position: 1, status: 1, isDeleted: false);
homePromotion.Sort = 9999; // 最高Sort值
dbContext.Promotions.Add(homePromotion);
// 创建团队页位置但禁用的宣传图高Sort值不应出现在结果中
var disabledPromotion = CreatePromotion(seed.Get + 200, position: 2, status: 0, isDeleted: false);
disabledPromotion.Sort = 9998; // 第二高Sort值
dbContext.Promotions.Add(disabledPromotion);
// 创建团队页位置但已删除的宣传图高Sort值不应出现在结果中
var deletedPromotion = CreatePromotion(seed.Get + 300, position: 2, status: 1, isDeleted: true);
deletedPromotion.Sort = 9997; // 第三高Sort值
dbContext.Promotions.Add(deletedPromotion);
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act
var result = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert:
// 1. 只返回团队页位置且启用且未删除的记录
if (result.Images.Count != 3) return false;
// 2. 不包含首页位置、禁用或已删除的记录
var excludedImageUrls = new HashSet<string>
{
homePromotion.ImageUrl,
disabledPromotion.ImageUrl,
deletedPromotion.ImageUrl
};
if (result.Images.Any(img => excludedImageUrls.Contains(img)))
{
return false;
}
// 3. 按Sort降序排列
var expectedOrder = teamPromotions
.OrderByDescending(p => p.Sort)
.Select(p => p.ImageUrl)
.ToList();
for (int i = 0; i < result.Images.Count; i++)
{
if (result.Images[i] != expectedOrder[i])
{
return false;
}
}
return true;
}
/// <summary>
/// Property 1: 排序稳定性
///
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
/// **Validates: Requirements 15.1**
/// </summary>
[Property(MaxTest = 50)]
public bool TeamInfoSortingIsStable(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建具有相同Sort值的团队页宣传图
var sameSort = seed.Get % 100;
for (int i = 0; i < 3; i++)
{
var promotion = CreatePromotionWithSort(seed.Get + i, sameSort);
dbContext.Promotions.Add(promotion);
}
dbContext.SaveChanges();
var service = new TeamService(dbContext, _mockLogger.Object);
// Act: 多次调用应该返回相同的顺序
var result1 = service.GetInfoAsync().GetAwaiter().GetResult();
var result2 = service.GetInfoAsync().GetAwaiter().GetResult();
// Assert: 两次调用返回的顺序应该相同
if (result1.Images.Count != result2.Images.Count) return false;
for (int i = 0; i < result1.Images.Count; i++)
{
if (result1.Images[i] != result2.Images[i]) return false;
}
return true;
}
#endregion
#region
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private MiAssessmentDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new MiAssessmentDbContext(options);
}
/// <summary>
/// 创建测试宣传图
/// </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值的测试宣传图团队页位置
/// </summary>
private Promotion CreatePromotionWithSort(int seed, int sort)
{
return new Promotion
{
Title = $"Test Promotion {seed}",
ImageUrl = $"https://example.com/promotion_{seed}.jpg",
Position = 2, // 团队页位置
Sort = sort,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
}
#endregion
}