DEV Community

Cover image for Teste de integração com SqlServer e Azure Devops
Tiago Brito for DEVz Wiz

Posted on

Teste de integração com SqlServer e Azure Devops

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

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() }); 
} 
Enter fullscreen mode Exit fullscreen mode

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

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

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:    
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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;
[CollectionDefinition("DatabaseTestCollection")] 
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> 
{ 
} 
Enter fullscreen mode Exit fullscreen mode

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;

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(); 
            } 
        } 
Enter fullscreen mode Exit fullscreen mode

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.

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() }); 
} 
Enter fullscreen mode Exit fullscreen mode

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.

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

        return entity; 
    } 
} 
Enter fullscreen mode Exit fullscreen mode

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

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

[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"); 
} 
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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.

# 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'
Enter fullscreen mode Exit fullscreen mode

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.


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 
Enter fullscreen mode Exit fullscreen mode

No Azure podemos visualizar os resultados

Alt Text

Top comments (0)