486 lines
16 KiB
C#
486 lines
16 KiB
C#
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
|
||
}
|