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() });
}
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:
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;
[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;
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.
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.
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 .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");
}
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
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.
# 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.
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
Top comments (0)