451 lines
15 KiB
C#
451 lines
15 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>
|
||
/// PlannerService 属性测试
|
||
/// 验证规划师服务的列表查询过滤和排序正确性
|
||
/// </summary>
|
||
public class PlannerServicePropertyTests
|
||
{
|
||
private readonly Mock<ILogger<PlannerService>> _mockLogger = new();
|
||
|
||
#region Property 1: 列表查询过滤正确性
|
||
|
||
/// <summary>
|
||
/// Property 1: 规划师列表只返回启用状态的记录
|
||
/// *For any* Planner list query, the returned items SHALL only include records
|
||
/// with Status=1 and IsDeleted=false.
|
||
///
|
||
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PlannerListOnlyReturnsEnabledRecords(PositiveInt seed)
|
||
{
|
||
// Arrange: 创建包含不同状态的Planner数据
|
||
using var dbContext = CreateDbContext();
|
||
|
||
// 创建启用的规划师
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
dbContext.Planners.Add(CreatePlanner(seed.Get + i, status: 1, isDeleted: false));
|
||
}
|
||
|
||
// 创建禁用的规划师
|
||
for (int i = 0; i < 2; i++)
|
||
{
|
||
dbContext.Planners.Add(CreatePlanner(seed.Get + 100 + i, status: 0, isDeleted: false));
|
||
}
|
||
|
||
// 创建已删除的规划师
|
||
for (int i = 0; i < 2; i++)
|
||
{
|
||
dbContext.Planners.Add(CreatePlanner(seed.Get + 200 + i, status: 1, isDeleted: true));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 调用服务方法
|
||
var result = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证返回结果只包含Status=1且未删除的记录
|
||
// 1. 返回的记录数应该等于启用且未删除的记录数
|
||
if (result.Count != 3) return false;
|
||
|
||
// 2. 验证数据库中确实存在被过滤掉的记录
|
||
var allPlanners = dbContext.Planners.ToList();
|
||
var disabledPlanners = allPlanners.Where(p => p.Status == 0 && !p.IsDeleted).ToList();
|
||
var deletedPlanners = allPlanners.Where(p => p.IsDeleted).ToList();
|
||
|
||
if (disabledPlanners.Count != 2 || deletedPlanners.Count != 2) return false;
|
||
|
||
// 3. 验证返回的记录中不包含禁用或已删除的记录
|
||
var returnedIds = result.Select(p => p.Id).ToHashSet();
|
||
var disabledIds = disabledPlanners.Select(p => p.Id).ToHashSet();
|
||
var deletedIds = deletedPlanners.Select(p => p.Id).ToHashSet();
|
||
|
||
return !returnedIds.Intersect(disabledIds).Any() && !returnedIds.Intersect(deletedIds).Any();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 1: 规划师列表不返回已删除的记录
|
||
///
|
||
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PlannerListExcludesDeletedRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
|
||
// 创建一个启用的规划师
|
||
var activePlanner = CreatePlanner(seed.Get, status: 1, isDeleted: false);
|
||
dbContext.Planners.Add(activePlanner);
|
||
|
||
// 创建已删除的规划师(即使Status=1)
|
||
var deletedPlanner = CreatePlanner(seed.Get + 1, status: 1, isDeleted: true);
|
||
dbContext.Planners.Add(deletedPlanner);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert: 已删除的记录不应出现在结果中
|
||
return result.Count == 1 && result[0].Id == activePlanner.Id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 1: 规划师列表不返回禁用状态的记录
|
||
///
|
||
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PlannerListExcludesDisabledRecords(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
|
||
// 创建一个启用的规划师
|
||
var enabledPlanner = CreatePlanner(seed.Get, status: 1, isDeleted: false);
|
||
dbContext.Planners.Add(enabledPlanner);
|
||
|
||
// 创建禁用的规划师
|
||
var disabledPlanner = CreatePlanner(seed.Get + 1, status: 0, isDeleted: false);
|
||
dbContext.Planners.Add(disabledPlanner);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert: 禁用的记录不应出现在结果中
|
||
return result.Count == 1 && result[0].Id == enabledPlanner.Id;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 2: 列表查询排序正确性
|
||
|
||
/// <summary>
|
||
/// Property 2: 规划师列表按Sort字段降序排列
|
||
/// *For any* Planner list query, the returned items SHALL be ordered by Sort
|
||
/// field in descending order.
|
||
///
|
||
/// **Feature: miniapp-api, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PlannerListSortedBySortDescending(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var random = new Random(seed.Get);
|
||
|
||
// 创建具有不同Sort值的规划师
|
||
var sortValues = new List<int>();
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
var sortValue = random.Next(1, 1000);
|
||
sortValues.Add(sortValue);
|
||
dbContext.Planners.Add(CreatePlannerWithSort(seed.Get + i, sortValue));
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert: 验证列表按Sort降序排列
|
||
if (result.Count < 2) return true;
|
||
|
||
for (int i = 0; i < result.Count - 1; i++)
|
||
{
|
||
// 获取当前和下一个规划师的Sort值
|
||
var currentPlanner = dbContext.Planners.First(p => p.Id == result[i].Id);
|
||
var nextPlanner = dbContext.Planners.First(p => p.Id == result[i + 1].Id);
|
||
|
||
if (currentPlanner.Sort < nextPlanner.Sort)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 2: 规划师列表排序稳定性
|
||
///
|
||
/// **Feature: miniapp-api, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool PlannerListSortingIsStable(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
|
||
// 创建具有相同Sort值的规划师
|
||
var sameSort = seed.Get % 100;
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var planner = CreatePlannerWithSort(seed.Get + i, sameSort);
|
||
dbContext.Planners.Add(planner);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act: 多次调用应该返回相同的顺序
|
||
var result1 = service.GetListAsync().GetAwaiter().GetResult();
|
||
var result2 = service.GetListAsync().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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 2: 规划师列表排序正确性 - 验证最大Sort值在最前
|
||
///
|
||
/// **Feature: miniapp-api, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool PlannerListFirstItemHasHighestSort(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var random = new Random(seed.Get);
|
||
|
||
// 创建具有不同Sort值的规划师
|
||
var planners = new List<Planner>();
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
var sortValue = random.Next(1, 1000);
|
||
var planner = CreatePlannerWithSort(seed.Get + i, sortValue);
|
||
planners.Add(planner);
|
||
dbContext.Planners.Add(planner);
|
||
}
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert: 第一个元素应该是Sort值最大的
|
||
if (result.Count == 0) return true;
|
||
|
||
var maxSort = planners.Max(p => p.Sort);
|
||
var firstPlanner = dbContext.Planners.First(p => p.Id == result[0].Id);
|
||
|
||
return firstPlanner.Sort == maxSort;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 综合属性测试
|
||
|
||
/// <summary>
|
||
/// Property 1: 空数据库返回空列表
|
||
///
|
||
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Fact]
|
||
public void EmptyDatabaseReturnsEmptyList()
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var planners = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert
|
||
Assert.Empty(planners);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 1: 所有记录都被过滤时返回空列表
|
||
///
|
||
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 50)]
|
||
public bool AllFilteredRecordsReturnsEmptyList(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
|
||
// 只创建不符合条件的记录
|
||
// 禁用的规划师
|
||
dbContext.Planners.Add(CreatePlanner(seed.Get, status: 0, isDeleted: false));
|
||
|
||
// 已删除的规划师
|
||
dbContext.Planners.Add(CreatePlanner(seed.Get + 1, status: 1, isDeleted: true));
|
||
|
||
// 禁用且已删除的规划师
|
||
dbContext.Planners.Add(CreatePlanner(seed.Get + 2, status: 0, isDeleted: true));
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var planners = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert: 列表应该为空
|
||
return planners.Count == 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 1 & 2: 过滤和排序同时正确
|
||
///
|
||
/// **Feature: miniapp-api, Property 1: 列表查询过滤正确性, Property 2: 列表查询排序正确性**
|
||
/// **Validates: Requirements 10.1**
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public bool FilteringAndSortingWorkTogether(PositiveInt seed)
|
||
{
|
||
// Arrange
|
||
using var dbContext = CreateDbContext();
|
||
var random = new Random(seed.Get);
|
||
|
||
// 创建启用的规划师(不同Sort值)
|
||
var enabledPlanners = new List<Planner>();
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
var sortValue = random.Next(1, 1000);
|
||
var planner = CreatePlannerWithSort(seed.Get + i, sortValue);
|
||
enabledPlanners.Add(planner);
|
||
dbContext.Planners.Add(planner);
|
||
}
|
||
|
||
// 创建禁用的规划师(高Sort值,不应出现在结果中)
|
||
var disabledPlanner = CreatePlanner(seed.Get + 100, status: 0, isDeleted: false);
|
||
disabledPlanner.Sort = 9999; // 最高Sort值
|
||
dbContext.Planners.Add(disabledPlanner);
|
||
|
||
// 创建已删除的规划师(高Sort值,不应出现在结果中)
|
||
var deletedPlanner = CreatePlanner(seed.Get + 200, status: 1, isDeleted: true);
|
||
deletedPlanner.Sort = 9998; // 第二高Sort值
|
||
dbContext.Planners.Add(deletedPlanner);
|
||
|
||
dbContext.SaveChanges();
|
||
|
||
var service = new PlannerService(dbContext, _mockLogger.Object);
|
||
|
||
// Act
|
||
var result = service.GetListAsync().GetAwaiter().GetResult();
|
||
|
||
// Assert:
|
||
// 1. 只返回启用且未删除的记录
|
||
if (result.Count != 3) return false;
|
||
|
||
// 2. 不包含禁用或已删除的记录
|
||
var returnedIds = result.Select(p => p.Id).ToHashSet();
|
||
if (returnedIds.Contains(disabledPlanner.Id) || returnedIds.Contains(deletedPlanner.Id))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 3. 按Sort降序排列
|
||
for (int i = 0; i < result.Count - 1; i++)
|
||
{
|
||
var currentPlanner = dbContext.Planners.First(p => p.Id == result[i].Id);
|
||
var nextPlanner = dbContext.Planners.First(p => p.Id == result[i + 1].Id);
|
||
|
||
if (currentPlanner.Sort < nextPlanner.Sort)
|
||
{
|
||
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 Planner CreatePlanner(int seed, int status, bool isDeleted)
|
||
{
|
||
return new Planner
|
||
{
|
||
Name = $"Test Planner {seed}",
|
||
Avatar = $"https://example.com/avatar_{seed}.jpg",
|
||
Introduction = $"Introduction for planner {seed}",
|
||
Price = 100.00m + (seed % 100),
|
||
Sort = seed % 100,
|
||
Status = status,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = isDeleted
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建指定Sort值的测试规划师
|
||
/// </summary>
|
||
private Planner CreatePlannerWithSort(int seed, int sort)
|
||
{
|
||
return new Planner
|
||
{
|
||
Name = $"Test Planner {seed}",
|
||
Avatar = $"https://example.com/avatar_{seed}.jpg",
|
||
Introduction = $"Introduction for planner {seed}",
|
||
Price = 100.00m + (seed % 100),
|
||
Sort = sort,
|
||
Status = 1,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now,
|
||
IsDeleted = false
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
}
|