Teste de integração com SqlServer e Azure Devops
Tags: testing, devops, azure, dotnet
Published: 2020-12-17 12:54:29

Há poucos dias atrás fiz uma implementação para integrar meus testes com o Sql Server. 

## Motivação

O que me levou a escrever esse tipo de teste foi: 

* Quero escrever um testes que seja o mais aderente ao ambiente de produção - ou seja, se vai rodar em Sql Server – tenho que escrever um testes que rode em Sql Server 
* Quero testar consultas mais complexas; 
* Quero testar cada método da minha classe de repositório, com cenário próprio, sem precisar rodar o sistema inteiro para fazer realizar este teste; 
* Possibilidade de testes transacionais 

### Para rodar os testes segui os seguintes passos: 

* Montei um Sql Server em um container; 
* Escrevi um código para criação da estrutura de banco, cenário e execução dos teste de repositórios; 
* Escrevi os testes propriamente ditos; 
* Configurei o pipeline do Azure para subir uma instância do Sql Server em tempo de build antes de rodar os testes integrados com repositórios. 

### Exemplo do método de repositório que quero testar 

```csharp
public async Task<IEnumerable<User>> GetByEmailAsync(string email) 
{ 
     var query = @" 
SELECT  
      u.Id, u.FirstName, u.LastName, u.PersonalIdentificationNumber, 
      u.Email, u.ProfileCode, u.DateCreated 
FROM  
          [dbo].[User] u                 
WHERE  
    u.Email = @Email 
    AND u.TenantId = @TenantId"; 

    return await _dapperContext.DapperConnection 
                   .QueryAsync<User>( 
                        query, new { Email = email, TenantId = this.TenantId() }); 
} 
```

## SqlServer no meu ambiente de desenvolvimento

Criei um container (docker) para instância do Sql Server. Para subir o Sql Server disponibilizo meu docker-compose que utilizei para inicialização do container; 

Meu sistema operacional é o Windows então, utilizei o _docker_ for windows disponível [Aqui](https://www.docker.com/products/docker-desktop)

Salve um arquivo em alguma pasta no seu computador com esse conteúdo. No meu ambiente, salvei aqui [C:\_devz\dockers\sqlserver] 

**docker-compose.yml**

```yml
version: '3' 

services: 
    db: 
        image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu 
        environment: 
            SA_PASSWORD: "Q1w2e3r4!" 
            ACCEPT_EULA: Y 
            MSSQL_PID: Express 
        ports: 
            - "1433:1433" 
        volumes: 
            - mssql-volume:/var/lib/mssql 
        networks: 
            - mssql_docknet            
networks: 
    mssql_docknet: 
        driver: bridge                
volumes: 
    mssql-volume:    
```    

Para executar o container, execute no prompt como abaixo: 

```
> cd C:\_devz\Dockers\sqlserver [Esse é o local onde está o docker-compose.yml] 

> docker-compose up –d 
```

## Código para estrutura e conexão com banco de dados 

Para que meu teste funcionasse precisei de uma instância do Sql Server funcionando - instância que configurei acima; 

Para ter cenários e testes independentes, antes que os testes fossem executados: 

* Criei uma classe de contexto compartilhado que o xunit disponibiliza; 
* Esse contexto cria o banco de dados; 
* Esse contexto cria as tabelas configuradas no Entity; 

```csharp
[CollectionDefinition("DatabaseTestCollection")] 
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> 
{ 
} 
```

Com essa classe eu defini um contexto global para todos os meus testes. Essa classe é assim mesmo - não tem código nenhum em seu corpo. 

Na classe **DatabaseFixture** tenho as informações do banco de dados e a inicialização do banco; 

Nesta classe configurei as informações do Entity e do Dapper; 
Com essas informações, meu banco de dados de teste é criado; 

```csharp
public DapperContext _dapper; 
public IConfiguration _configuration; 

public DatabaseFixture() 
{ 
////minha string de conexão que se conecta com o container montado
    var connectionString = $"Server=localhost;Database=gamificacao_test;User ID=sa;Password=Q1w2e3r4!;Trusted_Connection=False;"; 
 
////Adicionei algumas configurações para rastrear problemas de banco ao rodar meus testes
    ContextOptions = new DbContextOptionsBuilder<EntityContext>() 
                        .UseSqlServer(connectionString) 
                        .EnableSensitiveDataLogging() 
          .UseLoggerFactory(LoggerFactory.Create(builder => { builder.AddDebug(); })) 
                        .Options; 

////Como se trata de um cenário de testes algumas coisas coloquei em memória por não ser relevante para o testes
    var myConfiguration = new Dictionary<string, string> 
    { 
        { "ConnectionStrings:GamificacaoDB", connectionString } 
    }; 

    _configuration = new ConfigurationBuilder() 
        .AddInMemoryCollection(myConfiguration) 
        .Build(); 

    IdentityServiceMock = new Mock<IIdentityService>(); 
    IdentityServiceMock.Setup(x => x.GetTenantId()) 
        .Returns("Wx1"); 

    Context = new EntityContext(ContextOptions, IdentityServiceMock.Object); 

    _dapper = new DapperContext(_configuration); 

    //// Cria o meu banco de testes
    InitializeDatabase();
} 

////Repositorios da minha API que pretendo testar
public ActionRepository ActionRepository {  

    get { return new ActionRepository(Context, _dapper, IdentityServiceMock.Object); } } 

public UserRepository UserRepository {  

    get { return new UserRepository(Context, _dapper, IdentityServiceMock.Object); } } 

protected virtual void InitializeDatabase() 
        { 
            using (var context = new EntityContext(ContextOptions, IdentityServiceMock.Object)) 
            { 
                context.Database.EnsureDeleted(); 
                context.Database.EnsureCreated(); 
            } 
        } 
```

## O repositório que pretendo testar 

Estou testando o repositório de usuários (_UserRepository_). O método em questão está utilizando _Dapper_ e minha consulta é muito simples. 

```csharp
public async Task<IEnumerable<User>> GetByEmailAsync(string email) 
{ 
     var query = @" 
SELECT  
      u.Id, u.FirstName, u.LastName, u.PersonalIdentificationNumber, 
      u.Email, u.ProfileCode, u.DateCreated 
FROM  
          [dbo].[User] u                 
WHERE  
    u.Email = @Email 
    AND u.TenantId = @TenantId"; 

    return await _dapperContext.DapperConnection 
                   .QueryAsync<User>( 
                        query, new { Email = email, TenantId = this.TenantId() }); 
} 
```

## O Teste 

Quando a instância do SqlServer iniciou não tenho nenhuma informação nas tabelas para executar os testes. Então, preciso criar esses dados. Para isso, utilizei as mesmas classes de criação de objetos que já temos em nossa estrutura. Veja no teste abaixo: Criei 1 Usuário. 

Para o contexto dos testes criei um método estendido para as entidades. Nesse método consigo inserir no banco de dados o que está na entidade. Isso facilita inclusão de dados nas tabelas sem a necessidade de chamar o repositório para inserir. 

```csharp
public static class EntityExtensions 
{ 
    public static T Persist<T>( 
           this T entity,  
           EntityContext context) where T : class 
    { 
        context.Add(entity); 
        context.SaveChanges(); 

        return entity; 
    } 
} 
``` 

Então, na linha onde temos **user.Persist(_databaseFixture.Context)** a tabela _User_ será persistida. 

O mesmo aconteceria com outros objetos onde chamando o <Entidade>.Persist(). 

```csharp
[Fact] 
public async Task GetByMail_TestAsync() 
{ 
    //Cenario 
    var tiago = new Domain.Models.User 
    { 
        Email = "tiagobrito@wizsolucoes.com.br", 
        FirstName = "Tiago", 
        LastName = "Brito", 
        ProfileCode = "A", 
        PersonalIdentificationNumber = "15476338731" 
    }; 

    tiago.Persist(_databaseFixture.Context); 

    //Execução 
    var userToCompare = await _databaseFixture.UserRepository                                 .GetByEmailAsync("tiagobrito@wizsolucoes.com.br"); 

     ///Validação 
    userToCompare.ElementAt(0).FirstName.Should().Be("Tiago"); 
    userToCompare.ElementAt(0).LastName.Should().Be("Brito"); 
} 
```

```csharp

 var userToCompare = await _databaseFixture.UserRepository                           .GetByEmailAsync("tiagobrito@wizsolucoes.com.br"); 

```

Na linha acima temos a execução do repositório. Nesta etapa, todos os meus dados já estão persistidos e, se alimentei as tabela corretamente e minha classe de repositório estiver com a consulta correta, os dados virão. Estamos testando apenas essa unidade. Fica bem simples e não preciso rodar o sistema inteiro. 

Para validar os retornos utilizamos o que já conhecemos  

Eu utilizo uma biblioteca para _assert_ fluente – considero que a leitura do teste fica bem legal. pode ser encontrada [Aqui](https://fluentassertions.com/introduction)

```csharp            
userToCompare.ElementAt(0).FirstName.Should().Be("Tiago"); 
userToCompare.ElementAt(0).LastName.Should().Be("Brito"); 

```

## Configuração do pipeline para testes de integração com Sql Sever no Azure 

Como mencionado no primeiro tópico, utilizo um container para subir uma instancia do Sql Server Express e realizar os testes. No Azure Devops fiz o mesmo: no arquivo **azure-pipelines.yml** do projeto descrevi o recurso que precisava nas extensões, segundo o novo modelo de pipeline, identifiquei o serviço que vou utilizar. No caso, o Sql Server. 

```yml
# ASP.NET Core
# Build a Web project that uses ASP.NET Core.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
# YAML reference:
# https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema

variables: 
    azResourceName: 'meu-projeto'

resources:
  repositories:
    - repository: dotnettemplate
      type: git  
      name: meurepositorio/modelos
      ref: refs/tags/v1.1
  containers:
  - container: mssql
    image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu
    ports: 
      - 1433:1433
    options: -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Q1w2e3r4!' -e 'MSSQL_PID=Express'

pool:
  vmImage: 'ubuntu-latest'

schedules:
  - cron: "0 0 * * *"
    displayName: Build Noturno
    always: true
    branches:
      include:
        - master

trigger:
  batch: true
  paths:
    exclude:
    - README.md

stages:
- stage: Build
  pool: Wiz Hosted Ubuntu 1604
  jobs:
    - job:
      continueOnError: true
      services:
        localhostsqlserver: mssql
      steps:
          #Essas linhas com powershell são apenas para testes. podem ser removidas
        - task: PowerShell@2
          displayName: 'delay 10'
          inputs:
            targetType: 'inline'
            script: |
              # Write your PowerShell commands here.
              start-sleep -s 10
        - task: CmdLine@2
          inputs:
            script: 'sqlcmd -S localhost -d master -U sa -P Q1w2e3r4! -Q "SELECT @@version;"'
         #Esse template compila aplicações .net
        - template: dotnetcore.yml@dotnettemplate
- stage: Uat
  condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'releases')))
  dependsOn: [Build]
  jobs:
    - deployment: 
      environment: staging
      strategy:
        runOnce:
          deploy:                              
            steps:
            - task: AzureRmWebAppDeployment@4
              displayName: Publish 
              inputs:
                ConnectionType: 'AzureRM'
                azureSubscription: 'xxx'
                appType: 'webApp'
                WebAppName: '$(azResourceName)-hml-api'
                packageForLinux: '$(Pipeline.Workspace)/drop/**/*.zip'
```

O YML abaixo é bem simplificado e é o que utilizamos na Wiz. Isso porque nossa equipe de governança trabalhou algum tempo para torná-lo simples assim.

```yml

resources: 
  repositories: 
  - repository: coretemplate 
    type: git 
    name: meu/repositorio 

  containers: 
  - container: mssql 
    image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu 
    env: 
      ACCEPT_EULA: Y 
      SA_PASSWORD: Q1w2e3r4! 
      MSSQL_PID: Express 
    ports:  
      - 1433:1433 
    options: --name mssql 

pool: 
  vmImage: 'ubuntu-latest' 

schedules: 
  - cron: "0 0 * * *" 
    displayName: Build Noturno 
    always: true 
    branches: 
      include: 
        - master 
trigger: 
  batch: true 
  paths: 
    exclude: 
    - README.md 

extends: 
  template: main.yml@coretemplate 
  parameters: 
    technology: 'dotnetcore' 
    dotnetcoreAppType: 'apiApp' 
    azResourceName: 'gamificacao' 
    azSubscriptionUAT: 'xxx' 
    azSubscriptionPRD: 'xxx' 
    dotnetcoreDotNetVersion: '3.x' 
    dotnetcoreBuildProject: '**/*[API].csproj' 
    dotnetcoreBuildConfiguration: Release 
    dotnetcoreTestProject: '**/*[Tt]ests/*.csproj' 
    dotnetcoreNugetFeed: 09b2821a-2950-4eff-a722-dbc8adf4da55 
    buildServices: 
      mssql: mssql 
    releaseServices: 
      mssql: mssql 
``` 
 
## No Azure podemos visualizar os resultados

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0ac21x7kk4a399r9af4v.PNG)