|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- # 用到的组件
-
-
-
- 本篇涉及的测试形式:
-
- - `Subcutaneous`测试:最接近`UI`层的测试,聚焦在输入和输出,非常适合逻辑和UI分离的场景,这样就避免进行UI测试,容易写也容易维护
- - `Integration`集成测试:
- - `Unit`单元测试:
-
-
-
- ## `FluentAssertions`组件
-
- - 支持非常自然地规定自动测试的结果
- - 更好的可读性
- - 更好的测试失败说明
- - 避免调试
- - 更有生产力,更简单
-
-
-
- ## `Moq`组件
-
- - 为`.NET`而来
- - 流行并且友好
- - 支持`Mock`类和接口
- - 强类型,这样可以避免和系统关键字冲突
- - 简单易用
-
-
-
- ## `EF Core InMemory`组件
-
- - 内置数据库引擎
- - 内存数据库
- - 适用于测试场景
- - 轻量级无依赖
-
-
-
- ## `Respawn`组件
-
- - 为集成测试准备的数据库智能清洗
- - 避免删除数据或回滚事务
- - 把数据库恢复到一个干净的某个节点
- - 忽略表和`Schema`,比如忽略`_EFMigrationsHistory`
- - 支持`SQL Server`, `Postgres`,`MySQL`
-
-
-
- # 界面
-
- ## 列表
-
- ![t1](F:\SourceCodes\DDWiki\专题\后端\测试\t1.png)
-
- ## 删除和更新
-
- ![t2](F:\SourceCodes\DDWiki\专题\后端\测试\t2.png)
-
- # 解决方案组件依赖
-
- - `.NET Core Template Package`
- - `ASP.NET Core 3.1`
- - `Entity Framework Core 3.1`
- - `ASP.NET Core Identity 3.1`
- - `Angular 9`
-
- # 文件结构
-
- - `Application`层
- - `Domain`层
- - `Infrastructure`层
- - `WebUI`层
- - 集成测试层
-
-
-
- # `WebUI`层
-
- `TodoListsController`
-
- ```
- [Authorize]
- public class TodoListsController : ApiController
- {
- [HttpGet]
- public async Task<IActionResult<TodoVm>> Get()
- {
- return await Mediator.Send(new GetTodosQuery());
- }
-
- [HttpGet("{id}")]
- public async Task<FileResult> Get(int id)
- {
- var vm = await Mediator.Send(new ExportTodosQuery {ListId=id});
- return File(vm.Content, vm.ContentType, vm.FileName);
- }
-
- [HttpPost]
- public async Task<IActionResult<int>> Create(CreateTodoListCommand command)
- {
- return await Mediator.Send(command);
- }
- }
-
-
- ```
-
- # `Application`层
-
- `GetTodosQuery.cs`
-
- ```
- public class GetTodosQuery : IRequest<TodosVm>
- {
-
- }
-
- public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, TodosVm>
- {
- private readonly IApplicationDbContext _context;
- private readonly IMapper _mapper;
-
- public GetTodosQueryHandler(IApplicationDbContext context, IMapper mapper)
- {
- _context = context;
- _mapper = mapper;
- }
-
- public async Task<TodosVm> Handler(GetTodosQuery request, CancellationToken cancellationToken)
- {
- return new TodosVm{
- PriorityLevels = Enum.GetValues(typeof(PriorityLevel))
- .Cast<PriorityLevel>()
- .Select(p => new PriorityLevelDto{Value=(int)p, Name=p.ToString()})
- .ToList,
- Lists = await _context.TodosLists
- .ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)
- .OrdreBy(t => t.Title)
- .ToListAsync(cacnellationToken);
- };
- }
- }
- ```
-
- 在`MediatR`的请求管道中做验证。
-
- ```
- public class RequestValidationBehavior<TRequest, TResponse> : IPiplelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
- {
- private readonly IENumerable<IValidator<TRequest>> _validators;
-
- public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
- {
- _validators = validators;
- }
-
- public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
- {
- if(_validators.Any())
- {
- var context = new ValidationContext(request);
-
- var failures = _validators
- .Select(v => v.Validate(context))
- .SelectMany(result => result.Errors)
- .Whre(f => f != null)
- .ToList();
-
- if(failures.Count != 0)
- {
- throw new ValidationException(failures);
- }
- }
- return next();
- }
- }
- ```
-
- 在`MediatR`的请求管道中处理异常。
-
- ```
- public class UnhandledExceptionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
- {
- private readonly ILogger<TRequest> _logger;
-
- public UnhandledExceptionBehaviour(ILogger<TRequest> logger)
- {
- _logger = logger;
- }
-
- public async Task<TResponse> Handle(TRequest request, CancellationTOken canellationToken, RequestHandlerDelegate<TResponse> next)
- {
- try
- {
- return await next();
- }
- catch(Exception ex)
- {
- var requestName = typeof(TRequest).Name;
- _logger.LogError();
- throw;
- }
- }
- }
- ```
-
- # 集成测试层
-
- 引用`WebUI`层
-
- ```
- dotnet add package FluentAssertions
- dotnet add package Moq
- dotnet add package Respawn
- ```
-
- `GetTodosTests.cs`
-
- ```
-
- using static Testing;
- public class GetTodosTests : TestBase //每次运行测试让数据库回到干净的状态
- {
- [Test]
- public async Task ShouldReturnAllListsAndAssociatedItems()
- {
- //Arrange
- await AddAsync(new TodoList{
- Title = "",
- Items = {
- new TodoItem {Title="",Done=true},
- new TodoItem {Title="",Done=true},
- new TodoItem {Title="",Done=true},
- new TodoItem {Title=""},
- new TodoItem {Title=""},
- new TodoItem {Title=""}
- }
- });
- var query = new GetTodosQuery();
-
- //Act
- TodosVm result = await SendAsync(query);
-
- //Assert
- result.Should().NotBeNull();
- result.List.Should().HaveCount(1);
- result.Lists.First().Items.Should().HaveCount();
- }
- }
- ```
-
- `CreateTodoListTests.cs`
-
- ```
-
- using static Testing;
- public class CreateTodListTests : TestBase//每次运行测试让数据库回到干净的状态
- {
- [Test]
- public void ShouldRequredMinimumFileds()
- {
- var command = new CrateTodoListCommand();
- FluenActions.Invoking(()=> SendAsync(command))
- .Should().Throw<ValidaitonException>();
- }
-
- [Test]
- public async Task ShouldRequiredUniqueTitle()
- {
- await SendAsync(new CreateTodoListCommand{
- Title = ""
- });
-
- var command = new CreateTodoListCommand
- {
- Title = ""
- };
-
-
- FluenActions.Invoking(() => SendAsync(comamnd))
- .Should().Throw<ValidationException>();
- }
-
- [Test]
- public async Task ShouldCreateTodoList()
- {
-
- var useId = await RunAsDefaultUserAsync();
- var command = new CreateTodoListCommand
- {
- Title = ""
- };
-
- var listeId = await SendAsync(command);
-
- var list = await FindAsync<TodoList>(listId);
-
- list.Should().NotBuNull();
- list.Title.Should().Be(command.Title);
- list.Created.Should().BeCloseTo(DateTime.Now, 10000);
- list.CreatedBy.Should().Be(userId);
- }
- }
- ```
-
- `UpdateDotListTests.cs`
-
- ```
- using static Testing;
- public class UpdateTodoListTests : TestBase
- {
- [Test]
- public void ShouldRequireValidTodoListId()
- {
- var command = new UpdateTodoListCommand
- {
- Id = 99,
- Title = "New Title"
- };
-
- FluenActions.Invoking(() => SendAsync(command)).Should().Throw<NotFoundException>();
- }
-
- [Test]
- public async Task ShouldRequireUniqueTitle()
- {
- var listId = await SendAsync(new CreateTodoListCommand{Title=""});
-
- await SendAsync(new CreateTodoListCommand={Title=""});
- var command = new UpdateTodoListCommand
- {
- Id = listId,
- Title = ""
- };
-
- FluentActions.Invoking(()=>
- SendAsync(command)).Should().Throw<ValidationException>().Where(ex => ex.Errors.ContainsKey("Title"))
- .And.Erros["Title"].Should().Contain("The specified title is already exists.");
- }
-
- [Test]
- public async Task ShouldUpdateTodoList()
- {
- var userId = await RunAsDefaultUserAsync();
- var listId = await SendAsync(new CreateTodoListCommand{
- Title = ""
- });
-
- var command = new UpdateTodoListCommand
- {
- Id = listId,
- Title= ""
- };
- await SendAsync(command);
- var list =await FindAsync<TodoList>(listId);
-
- list.Title.Should().Be(command.Title);
- list.LastModifiedBy.Should().NotBeNull();
- list.LastModifiedBy.Should().Be(userId);
- list.LastModified.Should().NotBeNull();
- list.LstModified.Should().BeCloaseTo(DateTime.Now, 1000);
- }
- }
- ```
-
- `Testing.cs`
-
- ```
- [SetUpFixture]
- public class Testing
- {
-
- private static IConfiguration _configuration;//配置
- private static IServiceScopeFactory _scopeFactory;//域
- private static Checkpoint _checkpoint;//Respawn
- private static string _currentUserId;
-
- [OneTimeSetUp]
- public void RunBeforeAnyTests()
- {
- var buidler = new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory)
- .AddJsonFile("appsettings.json", true, true)
- .AddEnvironmentVariables();
-
- _configuration = builder.Build();
-
- var services = new ServiceCollection();//看作是容器
- var startup = new Startup(_configuration);//Startup.cs需要IConfiguration
-
- //ServiceCollection需要宿主
- services.AddSignleton(Mock.Of<IWebHostEnvironment>(w =>
- w.ApplcationName == "CleanTesting.WebUI" &&
- w.EnvirnmentName == "Development"));
-
- startup.ConfigureServices(services);
-
- //Replace service registration for ICurrentUserService
- //Remove existing registration
- var currentUserServiceDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ICurrentUserService));
- service.Remove(currentUserServiceDescriptor);
-
- //Register testing version
- services.AddTransient(provider =>
- Mock.Of<ICurrentUserService>(s => s.UserId == _currentUserId));
-
- _scopeFactory = services.BuilderServiceProvider().GetService<IServiceScopeFactory>();
-
- _checkpoint = new Checkpoint{
- TablesToIgnore = new [] {"__EFMigrationsHistory"}
- };
- }
-
- public static async Task<string> RunAsDefaultUserAsync()
- {
- return await RundAsUserAsync("","");
- _currentUserId = null;
- }
-
- public static async Task<string> RunAsUserAsync(string userName, string password)
- {
- using var scope = _scopeFactory.CreateScope();
- var userManager = scope.ServiceProvider.GetService<UserManager<ApplicaitonUser>>();
-
- var user = new ApplicationUser{UserName=userName, Email = userName};
- var result = await userManager.CreateAsync(user, password);
- _currentUserId = user.Id;
- return _currentUserId;
- }
-
- public static async Task ResetState()
- {
- await _checkpoint.Reset(_configuration.GetConnectionString("DefaultConnection"));
- }
-
- public static async Task AddAsync<TEntity>(TEntity entity) where TEntity : class
- {
- using var scope = _scopeFactory.CreateScope();
- var context = scope.ServiceProvider.GetService<ApplicationDbContext>();
- context.Add(entity);
- await context.SaveChangesAsync();
- }
-
- public static async Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
- {
- using var scope = _scopeFacotry.CreateScope();
- var mediator = scope.ServiceProvider.GetService<IMediator>();
- return await mediator.Send(request);
- }
-
- public static async Task<TEntity> FindAsync<TEntity>(int id)
- {
- using var scope = _scopeFactory.CreateScope();
- var context = scope.ServiceProvider.GetService<ApplicationContext>();
- return await context.FindAsync<TEntity>(id);
- }
- }
- ```
-
- `appSetting.json`
-
- ```
- {
- "UseInMemoryDatabase":false,
- "ConnectionStrings":{
- "DefaultConnection" : "Server=(localdb)\\mssqllocaldb;Database=CleanTestingDb;Truested_Connection=True;MultipleActiveRestultSet"
- },
- "IdentityServer":{
- "Clients":{
- "CleanTesting.WebUI": {
- "Profile": "IdentityServerSPA"
- }
- },
- "Key":{
- "Type" : "Development"
- }
- }
- }
- ```
-
- `TestBase.cs`
-
- ```
- using static Testing;
- public calss TestBase
- {
- [SetUp]
- public async Task SetUp()
- {
- await ResetState();
- }
- }
- ```
-
- # 资源
-
- - `Clean Testing Sample Code`: `https://github.com/jasontaylordev/cleantesting`
- - `Rules to Better Unit Tests`: `https://rules.ssw.com.au/rules-to-better-unit-tests`(翻墙)
- - `Fixie Demo`: `https://github.com/fixie/fixie.demo`
|