O TNF foi construído voltado para que a boas praticas e os princípios de um código limpo fossem respeitados.
Para que isso aconteça um dos principais pontos a serem abordados aqui nesta seção são os testes de unidade e testes integrados.
Nesse tópico será descrito como realizar os testes de unidade, validando regra a regra de uma aplicação construída com o TNF também como os testes integrados passando de camada a camada de sua aplicação.
O TNF prove algumas estruturas para realização de testes em sua aplicação. Não é uma regra, mas sugerimos os seguintes frameworks para utilização em seu projeto.
É um framework free, voltado a comunidade open source, largamente utilizado para realizar testes na plataforma .NET. Ele suporte todas as tecnologias, atualmente fazendo parte do .NET Foundation.
Para sua instalação basta realizar a adição de 2 pacotes via nuget ao seu assembly: xunit e xunit.runner.visualstudio.
Referencia: https://xunit.github.io/
Consiste em um framework para realização de .NET mocking. Com ele é possível realizar mock de propriedades, classes, interfaces entre outras funcionalidades.
Para sua utilização, basta realizar a instalação do pacote NSubstitute via interface nuget.
Referencia: http://nsubstitute.github.io/
Framework com estrutura de asserção. Junto com o Xunit fornece uma gama de metodos de extensão para realizar os assert e validações de regras, objetos e comportamentos.
Para sua utilização, basta realizar a instalação do pacote Shouldly via interface nuget.
Referencia: https://github.com/shouldly/shouldly
Para todos os testes descritos a seguir considere o seguinte cenário:
Todos os cenários descritos a seguir estão dispostos em nosso github no endereço: https://github.com/totvsnetcore/tnf-samples/tree/master/TnfSample-Architecture
Nossa aplicação contem em sua camada de infraestrutura dois repositórios de dados: Entity Framework Core e Carol (SAS desenvolvido pela TOTVS).
Entidade no Entity Framework Core e Dto:
[AutoMap(typeof(CountryDto))] [Table("Countries")] public class Country : Entity { public const int MaxNameLength = 256; [Required] [MaxLength(MaxNameLength)] public string Name { get; set; } public Country() { } public Country(int id, string name) { Id = id; Name = name; } } public class CountryDto { public string Name { get; set; } }
Entidade no Carol e Dto:
[JsonObject("president")] public class PresidentEntity : CarolEntity { public string Name { get; set; } public string ZipCode { get; set; } public override object GetStagingMapping() { return new { name = "string", zipCode = "string" }; } } public class PresidentDto { public PresidentDto() { } public PresidentDto(string id, string name, string zipCode) { Id = id; Name = name; ZipCode = new ZipCode(zipCode); } public string Id { get; set; } public string Name { get; set; } public ZipCode ZipCode { get; set; } }
Nessa camada temos um serviço chamado WhiteHouseService responsável pela persistência das entidade de presidentes no Carol.
Esse serviço realiza a validação da entidade de domínio President, utilizando o padrão de builder do TNF com o uso de specifications.
Abaixo temos a implementação do serviço WhiteHouseService usando um repositório chamado IWhiteHouseRepositoriy implementado pela camada de infraestrutura consumindo dados do Carol.
// Domain service interface public interface IWhiteHouseService : IDomainService { Task<DtoResponseBase<PresidentDto>> InsertPresidentAsync(PresidentDto request); } // Domain service public class WhiteHouseService : DomainService<IWhiteHouseRepository>, IWhiteHouseService { private readonly IEventBus _eventBus; public WhiteHouseService(IWhiteHouseRepository repository, IEventBus eventBus) : base(repository) { _eventBus = eventBus; } public Task<DtoResponseBase<PresidentDto>> InsertPresidentAsync(PresidentDto request) { var response = new DtoResponseBase<PresidentDto>(); var builder = new PresidentBuilder() .WithId(item.Id) .WithName(item.Name); var build = builder.Build(); if (!build.Success) { response.AddNotifications(build.Notifications); return response; } var presidentCreated = await Repository.InsertPresidentAsync(presidents); response.Data = presidentCreated; // Trigger event _eventBus.Trigger(new PresidentCreatedEvent(presidentCreated)); return response; } }
Como podemos observar no código acima quando é feita a inserção de um presidente através do serviço a estrutura de builder é consumido validando assim se a entidade pode ou não utilizar o repositório de dados no Carol.
Caso a entidade esteja apta a ser cadastrada os dados são persistidos no repositório e um evento de criação de um presidente é disparado.
public class PresidentBuilder : Builder<President> { public PresidentBuilder() : base() { } public PresidentBuilder(President instance) : base(instance) { } public PresidentBuilder WithId(string id) { Instance.Id = id; return this; } public PresidentBuilder WithName(string name) { Instance.Name = name; return this; } public override BuilderResponse<President> Build() { // Entity specifications var shouldHaveName = new PresidentShouldHaveNameSpecification(); if (!shouldHaveName.IsSatisfiedBy(Instance)) { var notificationMessage = LocalizationHelper.GetString( AppConsts.LocalizationSourceName, President.Error.PresidentNameMustHaveValue); Response.AddNotification(President.Error.PresidentNameMustHaveValue, notificationMessage); } return base.Build(); } }
Temos dois serviços de aplicação em nosso cenário:
Abaixo podemos visualizar as interfaces de cada serviço de nosso cenário:
public interface ICountryAppService : IAsyncCrudAppService<CountryDto> { }
public interface IWhiteHouseAppService : IApplicationService { Task<PagingDtoResponse<PresidentDto>> GetAllPresidents(GellAllPresidentsRequestDto request); Task<PresidentDto> GetPresidentById(string id); Task<DtoResponseBase<List<PresidentDto>>> InsertPresidentAsync(List<PresidentDto> dtos, bool sync = true); Task<DtoResponseBase> UpdatePresidentAsync(PresidentDto dto); Task<DtoResponseBase> DeletePresidentAsync(string id); }
Agora é a hora de expormos toda a nossa arquitetura criada.
Para isso temos uma camada de serviços feita em AspNetCore com duas APIs: