Após testar cada unidade o próximo passo é testar um grupo de funcionalidades, como um todo, fazendo a integração de todos os módulos da aplicação.
Considere a estrutura de testes apresentada no capitulo anterior: Testes#Cenáriodetestes
Vamos começar criando os projetos onde estarão nossos testes integrados:
Em nosso visual studio: File -> New -> Project e vamos escolher a opção Class Library (.NET Core)
Para utilizar os frameworks de teste instale os seguintes pacotes via nuget:
Em nosso visual studio: File -> New -> Project e vamos escolher a opção Class Library (.NET Core)
Para utilizar os frameworks de teste instale os seguintes pacotes via nuget:
Para adicionar pacotes do TNF adicione nos sources do nuget o endereço: https://www.myget.org/F/tnf/api/v3/index.json
Se você construiu uma aplicação onde tenha utilizado uma infraestrutura com Entity Framework Core podemos utilizar um banco em memoria para realizar nossos testes integrados.
Vamos começar com a criação do modulo que fará o carregamento da estrutura de testes:
[DependsOn( typeof(AppModule), typeof(TnfTestBaseModule))] public class EfCoreAppTestModule : TnfModule { public override void PreInitialize() { Configuration.Modules .TnfEfCoreInMemory(IocManager.IocContainer) .RegisterDbContextInMemory<ArchitectureDbContext>(); } public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } }
O modulo "EfCoreAppTestModule" foi criado no projeto de testes sendo responsável por carregar toda a estrutura de sua aplicação para o teste integrado quando você trabalhar com testes e Entity Framework em memoria.
Note que o atributo do modulo "DependsOn" tem como referencia outro modulo chamado "AppModule". Este modulo é um modulo da camada de aplicação concreta a ser testada, contendo dependências de outras camadas como domínio e infraestrutura onde as entidades estão sendo persistidas com o Entity Framework Core.
Também temos como dependência o modulo "TnfTestBaseModule" do TNF carregando toda a estrutura de testes integrados.
O código exemplificado acima, carrega o modulo da camada a ser testada, configurando no método "PreInitialize" o uso do Entity Framework Core em memoria para o DbContext "ArchitectureDbContext" através do método "RegisterDbContextInMemory".
Definido o modulo de teste vamos criar nosso classe de setup chamada "EfCoreAppTestBase" realizando a herança da classe "TnfEfCoreIntegratedTestBase<Module>" que recebe um TnfModule (implementado anteriormente):
public class EfCoreAppTestBase : TnfEfCoreIntegratedTestBase<EfCoreAppTestModule> { protected override void InitializeIntegratedTest() { UsingDbContext<ArchitectureDbContext>( context => { context.Countries.Add(new Country(1, "Brasil")); context.Countries.Add(new Country(2, "EUA")); context.Countries.Add(new Country(3, "Uruguai")); context.Countries.Add(new Country(4, "Paraguai")); context.Countries.Add(new Country(5, "Venezuela")); }); } }
A classe TnfEfCoreIntegratedTestBase está contida no pacote Tnf.App.EntityFrameworkCore.TestBase.
Essa classe força a implementação do método "InitializeIntegratedTest" onde será definido o setup de dados.
A mesma classe expõe alguns métodos para inclusão de dados na memoria do contexto como podemos observar no exemplo acima usando a instrução "UsingDbContext<DbContext>".
Definida a classe de setup, vamos começar a escrita de nossos testes sobre os serviços de aplicação:
public class CountryAppServiceTests : EfCoreAppTestBase { private readonly ICountryAppService _countryAppService; public CountryAppServiceTests() { _countryAppService = LocalIocManager.Resolve<ICountryAppService>(); } [Fact] public void Service_Should_Not_Be_Null() { _countryAppService.ShouldNotBeNull(); } [Fact] public async Task Create_Item_With_Sucess() { var result = await _countryAppService.Create(new CountryDto() { Id = 6, Name = "Mexico" }); result.Name.ShouldBe("Mexico"); } [Fact] public async Task Get_Return_Item_With_Sucess() { var result = await _countryAppService.Get(new EntityDto<int>(1)); result.Id.ShouldBe(1); result.Name.ShouldBe("Brasil"); } }
Acima realizamos a herança da nossa classe que realiza o setup dos dados em nosso contexto em memoria. Podemos agora executar nossos testes em nosso serviço de aplicação normalmente em cima da estrutura em memoria do Entity Framework Core.
Cada teste executado no cenário acima será reproduzido de forma isolada.
Todo teste executado contém seu próprio contexto do Entity Framework Core rodando separadamente dos demais.
Quando estamos trabalhando com repositórios sem o uso de um ORM, precisamos fazer o mock da interface do repositório, simulando um cenário afim de explorar todo o funcionamento do sistema.
Em nosso caso vamos testar a infraestrutura do Carol de nosso cenário de testes.
Para isso o TNF possui uma extensão onde é possível realizar a substituição de uma injeção usando o NSubstitute para criar objeto de mock no injetor de dependência.
Vamos criar o modulo que irá carregar a estrutura de nosso teste e fazer a substituição do serviço no injetor de dependência.
[DependsOn( typeof(AppModule), typeof(TnfTestBaseModule))] public class NSubstituteAppTestModule : TnfModule { public override void PreInitialize() { // Mock repositories Configuration.ReplaceService<IWhiteHouseRepository>(() => { var instance = Substitute.For<IWhiteHouseRepository>(); var presidentToInsert = new PresidentDto("1", "New President", "55833479"); instance.InsertPresidentsAsync(Arg.Any<List<PresidentDto>>) .Returns(Task.FromResult(presidentToInsert)); IocManager.IocContainer.Register( Component .For<IWhiteHouseRepository>() .Instance(instance) .LifestyleTransient() ); }); } public override void Initialize() { IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly()); } }
O modulo "NSubstituteAppTestModule" foi criado no projeto de testes para carregar toda a estrutura de sua aplicação para o teste integrado.
Note que o atributo do modulo "DependsOn" tem como referencia outro modulo chamado "AppModule", carregando a camada de aplicação concreta a ser testada. Esse modulo também contém dependências de outras camadas como domínio e infraestrutura onde estão definidos nossos repositórios de dados.
Também temos como dependência o modulo "TnfTestBaseModule" para carregar a estrutura de testes integrados.
O código exemplificado acima, carrega o modulo da camada a ser testada, configurando no método "PreInitialize" com os objetos de mock. No nosso exemplo estamos mocando através da função ReplaceService usando o framework NSubstitute para criar um objeto de mock para esse repositório e registrá-lo no container de injeção de dependência.
Após a definição do módulo vamos criar a classe de Setup para cada teste integrado: para isso a classe abaixo nomeada de "NSubstituteAppTestBase" realiza a herança da classe "TnfEfCoreIntegratedTestBase<Module>" que recebe um TnfModule (implementado anteriormente):
public class NSubstituteAppTestBase : TnfIntegratedTestBase<NSubstituteAppTestModule> { }
A classe TnfIntegratedTestBase está contida no pacote Tnf.TestBase.
Definido nosso setup podemos implementar nossa classe e realizar os testes usando nosso repositório criado com NSubstitute.
public class WriteHouseAppServiceTests : NSubstituteAppTestBase { private readonly IWhiteHouseAppService _whiteHouseAppService; public WriteHouseAppServiceTests() { _whiteHouseAppService = LocalIocManager.Resolve<IWhiteHouseAppService>(); } [Fact] public async Task Should_Insert_President_With_Success() { // Act var response = await _whiteHouseAppService.InsertPresidentAsync(new PresidentDto("1", "New President", "12345678")); // Assert Assert.True(response.Success); } }
Podemos perceber que toda a estrutura de Injeção de dependência ainda é mantida inclusive nos testes.
O objeto LocalIocManager contém um wrapper que pode ser acessado para resolver toda a dependência das camadas carregadas em nosso teste através de nosso modulo que foi definido.
Com o NSubstitute criamos o objeto fazendo sua substituição dentro da estrutura de IoC do framework.
Assim temos o teste simulando toda a pilha de dependências do teste integrado.
Todos os testes integrados visto até o presente momento podem ser aplicados no teste da camada de serviço.
O teste desta camada é muito parecido aos testes anteriores de aplicação porque adiciona mais um nível ao teste que é exatamente onde a exposição da camada de aplicação ocorre.
Para testar nossa camada de serviços podemos unir os dois conceitos de testes partindo do principio que nossa aplicação pode ter N fontes de dados (Infraestrutura) e independente de qual sejam elas, precisamos criar testes para validar nossa regra de negocio.
Como nossa camada de serviço expõe toda uma infraestrutura usando fontes como Entity Framework Core e Carol, precisamos utilizar o Entity Framework Core em memoria em conjunto com objetos de mock.
Vamos começar com a criação de nosso modulo que fará o carregamento da estrutura dos testes:
[DependsOn( typeof(AppModule), typeof(TnfAspNetCoreTestBaseModule))] public class AppTestModule : TnfModule { public override void PreInitialize() { Configuration.Auditing.IsEnabledForAnonymousUsers = true; // Mock repositories Configuration.ReplaceService<IWhiteHouseRepository, WhiteHouseRepositoryMock>(); Configuration.Modules .TnfEfCoreInMemory(IocManager.IocContainer, IocManager.Resolve<IServiceProvider>()) .RegisterDbContextInMemory<ArchitectureDbContext>(); } public override void Initialize() { IocManager.RegisterAssemblyByConvention(typeof(AppTestModule).GetAssembly()); } } }
Note que nosso module ainda contem a mesma estrutura de antes usando a função ReplaceService para substituir o repositório que será injetado (nesse caso para elucidar não foi usado o NSubstitute, foi criado uma classe concreta, mas nada impede que ele seja usado como nos exemplos anteriores) e configurado o Entity Framework Core para trabalhar em memoria.
Nosso modulo agora contem dependências diferente pois vamos testar uma camada de AspNetCore e por isso devemos deixar explicito em nosso atributo "DependsOn" o uso do modulo TnfAspNetCoreTestBaseModule.
Como estamos criando um teste para uma aplicação ASP .NET Core que expoe nossas APIs, temos que criar uma classe de startup que fará a configuração de nosso pipeline:
public class StartupTest { public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc() .AddApplicationPart(typeof(TnfAspNetCoreModule).GetAssembly()) .AddApplicationPart(typeof(Tnf.Architecture.Web.Startup.WebModule).GetAssembly()) .AddControllersAsServices(); services.AddEntityFrameworkInMemoryDatabase(); // Configure Tnf and Dependency Injection return services.AddTnf<AppTestModule>(options => { // Test setup options.SetupTest(); }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseTnf(); //Initializes Tnf framework. app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } }
No exemplo acima podemos notar algumas coisas que foram configuradas:
Com nossa classe de startup criada podemos definir a classe de setup de cada teste integrado usando AspNetCore, Entity Framework em memoria e objetos mock:
public abstract class AppTestBase : TnfAspNetCoreIntegratedTestBase<StartupTest> { protected override void InitializeIntegratedTest() { IocManager.UsingDbContext<ArchitectureDbContext>( context => { context.Countries.Add(new Country(1, "Brasil")); context.Countries.Add(new Country(2, "EUA")); context.Countries.Add(new Country(3, "Uruguai")); context.Countries.Add(new Country(4, "Paraguai")); context.Countries.Add(new Country(5, "Venezuela")); }); } }
Note que aqui usamos para cada teste a herança da classe TnfAspNetCoreIntegratedTestBase<Startup>.
Essa classe auxilia em cenários de teste como esse onde precisamos testar um contexto AspNetCore, recebendo como parâmetro genérico a classe de startup com as configurações da aplicação de teste.
Essa classe contém um método que pode ser sobrescrito chamado "InitializeIntegratedTest", onde é possível realizar a configuração (setup) de cada teste executado.
No exemplo acima vemos que através do objeto IocManager conseguimos acessar o método UsingDbContext<DbContext> que recebe como parâmetro genérico o DbContext, para inserir nossos dados em memória.
Os objetos de mock foram configurados dentro do nosso modulo através de uma implementação concretra, mas como visto em testes anteriores podemos fazer o uso do NSubstitute.
Vamos agora definir uma classe de teste e realizar o teste em cima de nossa API:
public class WhiteHouseControllerTests : AppTestBase { [Fact] public void Should_Resolve_Controller() { ServiceProvider.GetService<WhiteHouseController>().ShouldNotBeNull(); } [Fact] public async Task GetAll_Presidents_With_Success() { // Act var response = await GetResponseAsObjectAsync<AjaxResponse<PagingDtoResponse<PresidentDto>>>( "/api/white-house?offset=0&pageSize=10", HttpStatusCode.OK ); // Assert Assert.True(response.Success); Assert.Equal(response.Result.Data.Count, 6); response.Result.Notifications.ShouldBeEmpty(); } [Fact] public async Task GetAll_Presidents_With_Invalid_Parameters() { // Act var response = await GetResponseAsObjectAsync<AjaxResponse<string>>( "/api/white-house?offset=0&pageSize=0", HttpStatusCode.BadRequest ); response.Success.ShouldBeTrue(); response.Result.ShouldBe($"Invalid parameter: pageSize"); } }
Em nossa classe de teste temos acesso a alguns métodos pela herança da classe TnfAspNetCoreIntegratedTestBase contida dentro do pacote Tnf.AspNetCore.TestBase para criação de testes em serviços.
Entre os métodos disponíveis podemos listar:
Esses métodos realizam a conversão da resposta para o formato esperado, onde é informado a url e o status Http de retorno esperado.
Em conjunto a isso nos testes acima foram usados o Xunit e o Shouldly, adicionando ainda mais suporte na hora de realizar as asserções em nossos retornos.
Neste teste temos o nível de integração máxima de nossa arquitetura, integrando as camadas de serviço, aplicação, domínio e infraestrutura.