mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Admin/QuestionPropertyTests.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

769 lines
26 KiB
C#

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>
/// Question 属性测试
/// 验证题目服务的正确性属性
/// </summary>
public class QuestionPropertyTests
{
private readonly Mock<ILogger<AssessmentService>> _mockLogger = new();
#region Property 5: Required Field Validation
/// <summary>
/// Property 5: 创建题目时缺少测评类型ID应该失败
/// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity.
///
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool RequiredFieldValidation_MissingAssessmentTypeId_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = new AssessmentService(dbContext, _mockLogger.Object);
var request = new CreateQuestionRequest
{
AssessmentTypeId = 0, // 无效的测评类型ID
QuestionNo = seed.Get,
Content = $"Test content {seed.Get}",
Sort = 1,
Status = 1
};
// Act & Assert
try
{
service.CreateQuestionAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
// 验证没有创建任何题目
var questionCount = dbContext.Questions.Count();
return ex.Code == ErrorCodes.ParamError && questionCount == 0;
}
}
/// <summary>
/// Property 5: 创建题目时缺少题号应该失败
/// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity.
///
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool RequiredFieldValidation_MissingQuestionNo_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 先创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = 0, // 无效的题号
Content = $"Test content {seed.Get}",
Sort = 1,
Status = 1
};
// Act & Assert
try
{
service.CreateQuestionAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
// 验证没有创建任何题目
var questionCount = dbContext.Questions.Count();
return ex.Code == ErrorCodes.ParamError && questionCount == 0;
}
}
/// <summary>
/// Property 5: 创建题目时缺少内容应该失败
/// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity.
///
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool RequiredFieldValidation_MissingContent_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 先创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = "", // 空内容
Sort = 1,
Status = 1
};
// Act & Assert
try
{
service.CreateQuestionAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
// 验证没有创建任何题目
var questionCount = dbContext.Questions.Count();
return ex.Code == ErrorCodes.ParamError && questionCount == 0;
}
}
/// <summary>
/// Property 5: 创建题目时内容为空白字符串应该失败
/// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity.
///
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool RequiredFieldValidation_WhitespaceContent_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 先创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = " ", // 空白字符串
Sort = 1,
Status = 1
};
// Act & Assert
try
{
service.CreateQuestionAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
// 验证没有创建任何题目
var questionCount = dbContext.Questions.Count();
return ex.Code == ErrorCodes.ParamError && questionCount == 0;
}
}
/// <summary>
/// Property 5: 创建题目时测评类型不存在应该失败
/// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity.
///
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool RequiredFieldValidation_NonExistentAssessmentType_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = new AssessmentService(dbContext, _mockLogger.Object);
var request = new CreateQuestionRequest
{
AssessmentTypeId = seed.Get + 1000, // 不存在的测评类型ID
QuestionNo = seed.Get,
Content = $"Test content {seed.Get}",
Sort = 1,
Status = 1
};
// Act & Assert
try
{
service.CreateQuestionAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
// 验证没有创建任何题目
var questionCount = dbContext.Questions.Count();
return ex.Code == ErrorCodes.AssessmentTypeNotFound && questionCount == 0;
}
}
/// <summary>
/// Property 5: 提供所有必填字段时创建题目应该成功
///
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool RequiredFieldValidation_AllRequiredFieldsProvided_ShouldSucceed(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 先创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = $"Test content {seed.Get}",
Sort = 1,
Status = 1
};
// Act
try
{
var questionId = service.CreateQuestionAsync(request).GetAwaiter().GetResult();
// Assert: 验证题目已创建
var createdQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == questionId);
return createdQuestion != null
&& createdQuestion.AssessmentTypeId == assessmentType.Id
&& createdQuestion.QuestionNo == seed.Get
&& createdQuestion.Content == $"Test content {seed.Get}";
}
catch (BusinessException)
{
return false; // 不应该抛出异常
}
}
#endregion
#region Property 8: Unique Constraint Enforcement (QuestionNo)
/// <summary>
/// Property 8: 同一测评类型内创建重复题号应该失败
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 100)]
public bool UniqueQuestionNoEnforcement_DuplicateQuestionNoInSameType_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 先创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
// 创建一个已存在的题目
var existingQuestion = new Question
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = $"Existing question {seed.Get}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Questions.Add(existingQuestion);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 尝试创建具有相同题号的题目
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get, // 使用相同的题号
Content = $"New question {seed.Get}",
Sort = 2,
Status = 1
};
// Assert
try
{
service.CreateQuestionAsync(request).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.QuestionNoExists;
}
}
/// <summary>
/// Property 8: 不同测评类型内可以使用相同题号
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 100)]
public bool UniqueQuestionNoEnforcement_SameQuestionNoInDifferentTypes_ShouldSucceed(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建两个测评类型
var assessmentType1 = CreateAssessmentType(dbContext, seed.Get, "TYPE_A");
var assessmentType2 = CreateAssessmentType(dbContext, seed.Get + 1000, "TYPE_B");
// 在第一个测评类型中创建题目
var existingQuestion = new Question
{
AssessmentTypeId = assessmentType1.Id,
QuestionNo = seed.Get,
Content = $"Question in type 1 {seed.Get}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Questions.Add(existingQuestion);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 在第二个测评类型中创建具有相同题号的题目
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType2.Id,
QuestionNo = seed.Get, // 使用相同的题号,但在不同的测评类型中
Content = $"Question in type 2 {seed.Get}",
Sort = 1,
Status = 1
};
// Assert
try
{
var newQuestionId = service.CreateQuestionAsync(request).GetAwaiter().GetResult();
var newQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == newQuestionId);
return newQuestion != null
&& newQuestion.AssessmentTypeId == assessmentType2.Id
&& newQuestion.QuestionNo == seed.Get;
}
catch (BusinessException)
{
return false; // 不应该抛出异常
}
}
/// <summary>
/// Property 8: 更新题目时使用已存在的题号应该失败
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 100)]
public bool UniqueQuestionNoEnforcement_UpdateWithDuplicateQuestionNo_ShouldFail(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
// 创建两个题目
var question1 = new Question
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = $"Question 1 {seed.Get}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
var question2 = new Question
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get + 1,
Content = $"Question 2 {seed.Get}",
Sort = 2,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Questions.AddRange(question1, question2);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 尝试将 question2 的题号更新为 question1 的题号
var updateRequest = new UpdateQuestionRequest
{
Id = question2.Id,
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get, // 使用 question1 的题号
Content = $"Updated question 2 {seed.Get}",
Sort = 2,
Status = 1
};
// Assert
try
{
service.UpdateQuestionAsync(updateRequest).GetAwaiter().GetResult();
return false; // 应该抛出异常
}
catch (BusinessException ex)
{
return ex.Code == ErrorCodes.QuestionNoExists;
}
}
/// <summary>
/// Property 8: 更新题目时保持自身题号不变应该成功
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 100)]
public bool UniqueQuestionNoEnforcement_UpdateWithSameQuestionNo_ShouldSucceed(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
// 创建一个题目
var question = new Question
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = $"Original question {seed.Get}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Questions.Add(question);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 更新题目,保持题号不变
var updateRequest = new UpdateQuestionRequest
{
Id = question.Id,
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get, // 保持相同的题号
Content = $"Updated question {seed.Get}",
Sort = 2,
Status = 1
};
var result = service.UpdateQuestionAsync(updateRequest).GetAwaiter().GetResult();
// Assert
var updatedQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == question.Id);
return result
&& updatedQuestion != null
&& updatedQuestion.QuestionNo == seed.Get
&& updatedQuestion.Content == $"Updated question {seed.Get}";
}
/// <summary>
/// Property 8: 软删除的题目题号可以被重用
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 100)]
public bool UniqueQuestionNoEnforcement_DeletedQuestionNoCanBeReused(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
// 创建并软删除一个题目
var deletedQuestion = new Question
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = $"Deleted question {seed.Get}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = true // 已软删除
};
dbContext.Questions.Add(deletedQuestion);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 尝试创建具有相同题号的新题目
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get, // 使用已删除题目的题号
Content = $"New question {seed.Get}",
Sort = 1,
Status = 1
};
// Assert
try
{
var newQuestionId = service.CreateQuestionAsync(request).GetAwaiter().GetResult();
var newQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == newQuestionId);
return newQuestion != null
&& newQuestion.QuestionNo == seed.Get
&& !newQuestion.IsDeleted;
}
catch (BusinessException)
{
return false; // 不应该抛出异常
}
}
/// <summary>
/// Property 8: 批量导入时重复题号应该被检测并报错
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 50)]
public bool UniqueQuestionNoEnforcement_BatchImportWithDuplicateQuestionNo_ShouldReportError(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
// 创建一个已存在的题目
var existingQuestion = new Question
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get,
Content = $"Existing question {seed.Get}",
Sort = 1,
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
dbContext.Questions.Add(existingQuestion);
dbContext.SaveChanges();
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 批量导入包含重复题号的题目
var request = new BatchImportQuestionsRequest
{
AssessmentTypeId = assessmentType.Id,
Questions = new List<BatchImportQuestionItem>
{
new BatchImportQuestionItem
{
QuestionNo = seed.Get, // 与已存在的题目重复
Content = $"Import question 1 {seed.Get}",
Sort = 1,
Status = 1
},
new BatchImportQuestionItem
{
QuestionNo = seed.Get + 1, // 新题号
Content = $"Import question 2 {seed.Get}",
Sort = 2,
Status = 1
}
}
};
var result = service.BatchImportQuestionsAsync(request).GetAwaiter().GetResult();
// Assert: 应该有一个失败(重复题号),一个成功
return result.FailedCount == 1
&& result.SuccessCount == 1
&& result.Errors.Any(e => e.QuestionNo == seed.Get && e.ErrorMessage.Contains("已存在"));
}
/// <summary>
/// Property 8: 批量导入时导入数据内部重复题号应该被检测并报错
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 50)]
public bool UniqueQuestionNoEnforcement_BatchImportWithInternalDuplicates_ShouldReportError(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
// Act: 批量导入包含内部重复题号的题目
var request = new BatchImportQuestionsRequest
{
AssessmentTypeId = assessmentType.Id,
Questions = new List<BatchImportQuestionItem>
{
new BatchImportQuestionItem
{
QuestionNo = seed.Get,
Content = $"Import question 1 {seed.Get}",
Sort = 1,
Status = 1
},
new BatchImportQuestionItem
{
QuestionNo = seed.Get, // 与第一个重复
Content = $"Import question 2 {seed.Get}",
Sort = 2,
Status = 1
},
new BatchImportQuestionItem
{
QuestionNo = seed.Get + 1, // 新题号
Content = $"Import question 3 {seed.Get}",
Sort = 3,
Status = 1
}
}
};
var result = service.BatchImportQuestionsAsync(request).GetAwaiter().GetResult();
// Assert: 第一个成功,第二个失败(内部重复),第三个成功
return result.FailedCount == 1
&& result.SuccessCount == 2
&& result.Errors.Any(e => e.ErrorMessage.Contains("重复"));
}
/// <summary>
/// Property 8: 多个不同题号的题目应该都能创建成功
/// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique.
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 50)]
public bool UniqueQuestionNoEnforcement_MultipleUniqueQuestionNosAllSucceed(PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
// 创建一个测评类型
var assessmentType = CreateAssessmentType(dbContext, seed.Get);
var service = new AssessmentService(dbContext, _mockLogger.Object);
var createdIds = new List<long>();
// Act: 创建多个具有不同题号的题目
for (int i = 0; i < 3; i++)
{
var request = new CreateQuestionRequest
{
AssessmentTypeId = assessmentType.Id,
QuestionNo = seed.Get + i,
Content = $"Question {seed.Get}_{i}",
Sort = i,
Status = 1
};
try
{
var id = service.CreateQuestionAsync(request).GetAwaiter().GetResult();
createdIds.Add(id);
}
catch (BusinessException)
{
return false; // 不应该抛出异常
}
}
// Assert: 所有题目都应该创建成功
var allCreated = createdIds.Count == 3;
var allUnique = dbContext.Questions
.Where(q => q.AssessmentTypeId == assessmentType.Id && !q.IsDeleted)
.Select(q => q.QuestionNo)
.Distinct()
.Count() == 3;
return allCreated && allUnique;
}
#endregion
#region
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AdminBusinessDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AdminBusinessDbContext(options);
}
/// <summary>
/// 创建测评类型
/// </summary>
private AssessmentType CreateAssessmentType(AdminBusinessDbContext dbContext, int seed, string? codePrefix = null)
{
var code = codePrefix ?? $"CODE_{seed}";
var assessmentType = new AssessmentType
{
Name = $"Test Type {seed}",
Code = code,
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;
}
#endregion
}