mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Admin/MappingPropertyTests.cs
zpc 6bf2ea595c feat(admin-business): 完成后台管理系统全部业务模块
- 系统配置管理模块 (Config)
- 内容管理模块 (Banner, Promotion)
- 测评管理模块 (Type, Question, Category, Mapping, Conclusion)
- 用户管理模块 (User)
- 订单管理模块 (Order)
- 规划师管理模块 (Planner)
- 分销管理模块 (InviteCode, Commission, Withdrawal)
- 数据统计仪表盘模块 (Dashboard)
- 权限控制集成
- 服务注册配置

全部381个测试通过
2026-02-03 20:50:51 +08:00

707 lines
24 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.Admin.Business.Data;
using MiAssessment.Admin.Business.Entities;
using MiAssessment.Admin.Business.Models;
using MiAssessment.Admin.Business.Models.Assessment;
using MiAssessment.Admin.Business.Models.Common;
using MiAssessment.Admin.Business.Services;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Admin;
/// <summary>
/// Mapping 属性测试
/// 验证题目分类映射服务的正确性属性
/// </summary>
public class MappingPropertyTests
{
private readonly Mock<ILogger<AssessmentService>> _mockLogger = new();
#region Property 10: Mapping Relationship Bidirectionality
/// <summary>
/// Property 10: 通过题目ID查询映射应包含关联的分类ID
/// *For any* QuestionCategoryMapping, querying mappings by QuestionId SHALL include the CategoryId.
///
/// **Validates: Requirements 7.1**
/// </summary>
[Property(MaxTest = 100)]
public bool Bidirectionality_QueryByQuestion_ShouldIncludeCategoryId(PositiveInt seed)
{
// Arrange: 创建题目、分类和映射关系
using var dbContext = CreateDbContext();
var (assessmentTypeId, questionId, categoryId) = CreateTestData(dbContext, seed.Get);
// 创建映射关系
var mapping = new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = categoryId,
CreateTime = DateTime.Now
};
dbContext.QuestionCategoryMappings.Add(mapping);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 通过题目ID查询映射
var categories = service.GetMappingsByQuestionAsync(questionId).GetAwaiter().GetResult();
// Assert: 应该包含关联的分类ID
return categories.Any(c => c.Id == categoryId);
}
/// <summary>
/// Property 10: 通过分类ID查询映射应包含关联的题目ID
/// *For any* QuestionCategoryMapping, querying mappings by CategoryId SHALL include the QuestionId.
///
/// **Validates: Requirements 7.2**
/// </summary>
[Property(MaxTest = 100)]
public bool Bidirectionality_QueryByCategory_ShouldIncludeQuestionId(PositiveInt seed)
{
// Arrange: 创建题目、分类和映射关系
using var dbContext = CreateDbContext();
var (assessmentTypeId, questionId, categoryId) = CreateTestData(dbContext, seed.Get);
// 创建映射关系
var mapping = new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = categoryId,
CreateTime = DateTime.Now
};
dbContext.QuestionCategoryMappings.Add(mapping);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 通过分类ID查询映射
var questions = service.GetMappingsByCategoryAsync(categoryId).GetAwaiter().GetResult();
// Assert: 应该包含关联的题目ID
return questions.Any(q => q.Id == questionId);
}
/// <summary>
/// Property 10: 双向查询应返回一致的映射关系
/// 如果通过题目查询到分类A则通过分类A查询应能找到该题目
///
/// **Validates: Requirements 7.1, 7.2**
/// </summary>
[Property(MaxTest = 100)]
public bool Bidirectionality_ConsistentBothDirections(PositiveInt seed, PositiveInt categoryCount)
{
// 限制分类数量为1-5
var actualCategoryCount = (categoryCount.Get % 5) + 1;
// Arrange: 创建题目和多个分类
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
var categoryIds = new List<long>();
for (int i = 0; i < actualCategoryCount; i++)
{
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get + i);
categoryIds.Add(categoryId);
// 创建映射关系
var mapping = new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = categoryId,
CreateTime = DateTime.Now
};
dbContext.QuestionCategoryMappings.Add(mapping);
}
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 通过题目查询分类
var categoriesFromQuestion = service.GetMappingsByQuestionAsync(questionId).GetAwaiter().GetResult();
// Assert: 对于每个查询到的分类,反向查询应能找到原题目
foreach (var category in categoriesFromQuestion)
{
var questionsFromCategory = service.GetMappingsByCategoryAsync(category.Id).GetAwaiter().GetResult();
if (!questionsFromCategory.Any(q => q.Id == questionId))
{
return false;
}
}
return true;
}
/// <summary>
/// Property 10: 多对多映射关系的双向一致性
/// 多个题目关联同一分类时,通过分类查询应返回所有关联的题目
///
/// **Validates: Requirements 7.1, 7.2**
/// </summary>
[Property(MaxTest = 50)]
public bool Bidirectionality_ManyToMany_Consistency(PositiveInt seed, PositiveInt questionCount)
{
// 限制题目数量为2-5
var actualQuestionCount = (questionCount.Get % 4) + 2;
// Arrange: 创建多个题目和一个分类
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get);
var questionIds = new List<long>();
for (int i = 0; i < actualQuestionCount; i++)
{
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get + i);
questionIds.Add(questionId);
// 创建映射关系
var mapping = new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = categoryId,
CreateTime = DateTime.Now
};
dbContext.QuestionCategoryMappings.Add(mapping);
}
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 通过分类查询题目
var questionsFromCategory = service.GetMappingsByCategoryAsync(categoryId).GetAwaiter().GetResult();
// Assert: 应该返回所有关联的题目
if (questionsFromCategory.Count != actualQuestionCount) return false;
foreach (var questionId in questionIds)
{
if (!questionsFromCategory.Any(q => q.Id == questionId))
{
return false;
}
}
return true;
}
/// <summary>
/// Property 10: 软删除的题目不应出现在分类的映射查询结果中
///
/// **Validates: Requirements 7.2**
/// </summary>
[Property(MaxTest = 100)]
public bool Bidirectionality_DeletedQuestionNotInCategoryQuery(PositiveInt seed)
{
// Arrange: 创建题目、分类和映射关系
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get);
// 创建正常题目
var normalQuestionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
dbContext.QuestionCategoryMappings.Add(new QuestionCategoryMapping
{
QuestionId = normalQuestionId,
CategoryId = categoryId,
CreateTime = DateTime.Now
});
// 创建已删除的题目
var deletedQuestion = new Question
{
AssessmentTypeId = assessmentTypeId,
QuestionNo = seed.Get + 1000,
Content = $"Deleted Question {seed.Get}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = true // 已软删除
};
dbContext.Questions.Add(deletedQuestion);
dbContext.SaveChanges();
dbContext.QuestionCategoryMappings.Add(new QuestionCategoryMapping
{
QuestionId = deletedQuestion.Id,
CategoryId = categoryId,
CreateTime = DateTime.Now
});
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 通过分类查询题目
var questions = service.GetMappingsByCategoryAsync(categoryId).GetAwaiter().GetResult();
// Assert: 只应该返回正常的题目,不应包含已删除的题目
return questions.Count == 1 && questions[0].Id == normalQuestionId;
}
/// <summary>
/// Property 10: 软删除的分类不应出现在题目的映射查询结果中
///
/// **Validates: Requirements 7.1**
/// </summary>
[Property(MaxTest = 100)]
public bool Bidirectionality_DeletedCategoryNotInQuestionQuery(PositiveInt seed)
{
// Arrange: 创建题目和分类
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
// 创建正常分类
var normalCategoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get);
dbContext.QuestionCategoryMappings.Add(new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = normalCategoryId,
CreateTime = DateTime.Now
});
// 创建已删除的分类
var deletedCategory = new ReportCategory
{
AssessmentTypeId = assessmentTypeId,
ParentId = 0,
Name = $"Deleted Category {seed.Get}",
Code = $"DELETED_{seed.Get}_{Guid.NewGuid():N}".Substring(0, 20),
CategoryType = 1,
ScoreRule = 1,
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = true // 已软删除
};
dbContext.ReportCategories.Add(deletedCategory);
dbContext.SaveChanges();
dbContext.QuestionCategoryMappings.Add(new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = deletedCategory.Id,
CreateTime = DateTime.Now
});
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 通过题目查询分类
var categories = service.GetMappingsByQuestionAsync(questionId).GetAwaiter().GetResult();
// Assert: 只应该返回正常的分类,不应包含已删除的分类
return categories.Count == 1 && categories[0].Id == normalCategoryId;
}
#endregion
#region Property 11: Batch Operation Atomicity
/// <summary>
/// Property 11: 批量更新成功时所有映射都应被更新
/// *For any* batch update of question-category mappings, all mappings SHALL be updated successfully.
///
/// **Validates: Requirements 7.6**
/// </summary>
[Property(MaxTest = 100)]
public bool Atomicity_SuccessfulUpdate_AllMappingsUpdated(PositiveInt seed, PositiveInt categoryCount)
{
// 限制分类数量为1-5
var actualCategoryCount = (categoryCount.Get % 5) + 1;
// Arrange: 创建题目和多个分类
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
var categoryIds = new List<long>();
for (int i = 0; i < actualCategoryCount; i++)
{
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get + i);
categoryIds.Add(categoryId);
}
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 批量更新映射
var request = new BatchUpdateMappingsRequest
{
QuestionId = questionId,
CategoryIds = categoryIds
};
try
{
var result = service.BatchUpdateMappingsAsync(request).GetAwaiter().GetResult();
if (!result) return false;
// Assert: 验证所有映射都已创建
var mappings = dbContext.QuestionCategoryMappings
.Where(m => m.QuestionId == questionId)
.ToList();
if (mappings.Count != actualCategoryCount) return false;
foreach (var categoryId in categoryIds)
{
if (!mappings.Any(m => m.CategoryId == categoryId))
{
return false;
}
}
return true;
}
catch (BusinessException)
{
return false;
}
}
/// <summary>
/// Property 11: 批量更新应替换所有现有映射
///
/// **Validates: Requirements 7.6**
/// </summary>
[Property(MaxTest = 100)]
public bool Atomicity_UpdateReplacesExistingMappings(PositiveInt seed)
{
// Arrange: 创建题目和分类,并创建初始映射
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
// 创建初始分类和映射
var oldCategoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get);
dbContext.QuestionCategoryMappings.Add(new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = oldCategoryId,
CreateTime = DateTime.Now
});
dbContext.SaveChanges();
// 创建新分类
var newCategoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get + 100);
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 批量更新映射(用新分类替换旧分类)
var request = new BatchUpdateMappingsRequest
{
QuestionId = questionId,
CategoryIds = new List<long> { newCategoryId }
};
try
{
var result = service.BatchUpdateMappingsAsync(request).GetAwaiter().GetResult();
if (!result) return false;
// Assert: 验证旧映射已删除,新映射已创建
var mappings = dbContext.QuestionCategoryMappings
.Where(m => m.QuestionId == questionId)
.ToList();
// 应该只有一个映射(新的)
if (mappings.Count != 1) return false;
if (mappings[0].CategoryId != newCategoryId) return false;
// 旧映射应该不存在
var oldMappingExists = dbContext.QuestionCategoryMappings
.Any(m => m.QuestionId == questionId && m.CategoryId == oldCategoryId);
return !oldMappingExists;
}
catch (BusinessException)
{
return false;
}
}
/// <summary>
/// Property 11: 批量更新失败时不应有任何映射被更新(回滚)
/// 当提供无效的分类ID时所有操作应回滚
///
/// **Validates: Requirements 7.6**
/// </summary>
[Property(MaxTest = 100)]
public bool Atomicity_FailedUpdate_NoMappingsChanged(PositiveInt seed)
{
// Arrange: 创建题目和分类,并创建初始映射
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
// 创建初始分类和映射
var existingCategoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get);
dbContext.QuestionCategoryMappings.Add(new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = existingCategoryId,
CreateTime = DateTime.Now
});
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 尝试批量更新映射包含无效的分类ID
var request = new BatchUpdateMappingsRequest
{
QuestionId = questionId,
CategoryIds = new List<long> { existingCategoryId, 999999 } // 999999 是无效的分类ID
};
try
{
service.BatchUpdateMappingsAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
// Assert: 验证原有映射未被修改
if (ex.Code != ErrorCodes.CategoryNotFound) return false;
var mappings = dbContext.QuestionCategoryMappings
.Where(m => m.QuestionId == questionId)
.ToList();
// 原有映射应该保持不变
return mappings.Count == 1 && mappings[0].CategoryId == existingCategoryId;
}
}
/// <summary>
/// Property 11: 空分类列表应清除所有映射
///
/// **Validates: Requirements 7.5, 7.6**
/// </summary>
[Property(MaxTest = 100)]
public bool Atomicity_EmptyCategoryList_ClearsAllMappings(PositiveInt seed)
{
// Arrange: 创建题目和分类,并创建初始映射
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
// 创建多个初始映射
for (int i = 0; i < 3; i++)
{
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get + i);
dbContext.QuestionCategoryMappings.Add(new QuestionCategoryMapping
{
QuestionId = questionId,
CategoryId = categoryId,
CreateTime = DateTime.Now
});
}
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 批量更新映射(空分类列表)
var request = new BatchUpdateMappingsRequest
{
QuestionId = questionId,
CategoryIds = new List<long>() // 空列表
};
try
{
var result = service.BatchUpdateMappingsAsync(request).GetAwaiter().GetResult();
if (!result) return false;
// Assert: 验证所有映射都已删除
var mappings = dbContext.QuestionCategoryMappings
.Where(m => m.QuestionId == questionId)
.ToList();
return mappings.Count == 0;
}
catch (BusinessException)
{
return false;
}
}
/// <summary>
/// Property 11: 批量更新应去重分类ID
///
/// **Validates: Requirements 7.4, 7.6**
/// </summary>
[Property(MaxTest = 100)]
public bool Atomicity_DuplicateCategoryIds_ShouldBeDeduplicated(PositiveInt seed)
{
// Arrange: 创建题目和分类
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed.Get);
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 批量更新映射包含重复的分类ID
var request = new BatchUpdateMappingsRequest
{
QuestionId = questionId,
CategoryIds = new List<long> { categoryId, categoryId, categoryId } // 重复的分类ID
};
try
{
var result = service.BatchUpdateMappingsAsync(request).GetAwaiter().GetResult();
if (!result) return false;
// Assert: 验证只创建了一个映射(去重后)
var mappings = dbContext.QuestionCategoryMappings
.Where(m => m.QuestionId == questionId)
.ToList();
return mappings.Count == 1 && mappings[0].CategoryId == categoryId;
}
catch (BusinessException)
{
return false;
}
}
/// <summary>
/// Property 11: 对不存在的题目进行批量更新应失败
///
/// **Validates: Requirements 7.3, 7.6**
/// </summary>
[Property(MaxTest = 100)]
public bool Atomicity_InvalidQuestionId_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var assessmentTypeId = CreateAssessmentType(dbContext, seed.Get);
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 尝试对不存在的题目进行批量更新
var request = new BatchUpdateMappingsRequest
{
QuestionId = 999999, // 不存在的题目ID
CategoryIds = new List<long> { categoryId }
};
try
{
service.BatchUpdateMappingsAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.QuestionNotFound;
}
}
#endregion
#region
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning))
.Options;
return new AdminBusinessDbContext(options);
}
/// <summary>
/// 创建测评类型并返回ID
/// </summary>
private long CreateAssessmentType(AdminBusinessDbContext dbContext, int seed)
{
var assessmentType = new AssessmentType
{
Name = $"Test Assessment {seed}",
Code = $"TEST_{seed}_{Guid.NewGuid():N}".Substring(0, 20),
Price = 99.99m,
QuestionCount = 80,
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.AssessmentTypes.Add(assessmentType);
dbContext.SaveChanges();
return assessmentType.Id;
}
/// <summary>
/// 创建题目并返回ID
/// </summary>
private long CreateQuestion(AdminBusinessDbContext dbContext, long assessmentTypeId, int seed)
{
var question = new Question
{
AssessmentTypeId = assessmentTypeId,
QuestionNo = seed,
Content = $"Test Question {seed}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Questions.Add(question);
dbContext.SaveChanges();
return question.Id;
}
/// <summary>
/// 创建分类并返回ID
/// </summary>
private long CreateCategory(AdminBusinessDbContext dbContext, long assessmentTypeId, int seed)
{
var category = new ReportCategory
{
AssessmentTypeId = assessmentTypeId,
ParentId = 0,
Name = $"Test Category {seed}",
Code = $"CAT_{seed}_{Guid.NewGuid():N}".Substring(0, 20),
CategoryType = (seed % 8) + 1,
ScoreRule = (seed % 2) + 1,
Sort = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.ReportCategories.Add(category);
dbContext.SaveChanges();
return category.Id;
}
/// <summary>
/// 创建测试数据(测评类型、题目、分类)
/// </summary>
private (long assessmentTypeId, long questionId, long categoryId) CreateTestData(
AdminBusinessDbContext dbContext, int seed)
{
var assessmentTypeId = CreateAssessmentType(dbContext, seed);
var questionId = CreateQuestion(dbContext, assessmentTypeId, seed);
var categoryId = CreateCategory(dbContext, assessmentTypeId, seed);
return (assessmentTypeId, questionId, categoryId);
}
#endregion
}