(Update: Veja também o artigo irmão deste: Testando o banco de dados: com infra não Microsoft)

Eu testo minhas aplicações, e algo que não pode ficar de fora é o teste do banco de dados. Vocês sabem que eu não gosto de colocar regras de negócio no banco de dados (e isso é assunto para outra discussão), então para mim, testar o banco de dados é testar a interação da aplicação com o banco de dados. E eu uso ORMs, ou seja, na verdade, o que eu preciso testar é a se o meu mapeamento está funcionando e se o schema do banco está correto, de acordo com o que a aplicação espera.

Neste post eu vou mostrar como fazer isso com infra Microsoft, ou seja, com Data Dude (VS Database) e Entity Framework. No próximo vou mostrar com alguns componentes open source. O próximo vem semana que vem.

Você só precisa de um Visual Studio 2010 Premium, porque na edição professional não vem o Data Dude, que é o antigo VS Database edition, que não existe mais. Ele tem um projeto de banco de dados, e vou usá-lo.

Mas antes de mais nada…

Como testar algo que interage com o banco de dados? É óbvio que se interage com o BD, não pode ser um teste unitário, tem que ser um teste integrado, nesse caso, integrado com o banco. O que testar?

Os testes vão manipular o banco, vão incluir, alterar, excluir, e consultar, ou seja, vão fazer operações CRUD. Quando um teste altera os dados do banco pode ser que ele altere o resultado de outro teste. Por exemplo, em um teste eu incluo um item em uma nota fiscal e salvo, antes ela tinha 10, agora tem 11. Em outro teste eu consulto a mesma nota, e verifico se ela tem 10 itens, o teste falha, porque o teste que inseriu o teste rodou antes. Eu até poderia determinar a ordem dos testes, mas isso deixaria meu plano de testes confuso e ruim: jamais poderia rodar um único teste sem antes rodar os outros. Testes devem rodar sempre independentes uns dos outros. Como resolver? A cada teste o banco deve ter seus dados reiniciados. Como subir estes dados? Já vi pessoas usarem transações, e dar rollback no final, mas isso é no mínimo incompleto: quem sobe os dados iniciais? É feito na mão? Além disso, o esquema tem que estar atualizado. Não dá pra testar se falta uma tabela, uma coluna, ou se há alguma diferença. Nossa infra de testes deve resolver esse problema.

Primeiro vamos resolver o problema do esquema. O Data Dude tem um projeto de banco de dados. Vejam lá, File > New > Project > Database > SQL Server > SQL Server 2008 Database Project (clique nas imagens para ampliar):

image

Esse projeto cria todo o esquema que você vai precisar e também os dados. Não vou explicar aqui o que é o Data Dude, mas confie em mim, funciona e é muito legal. Para mais informações, baixe o training kit, baixe a VM, e/ou veja online a documentação.

Criei também um projeto class library e fiz um mapeamento simples no EF, vejam só:

image

Esse mapeamento já gerou a classe de produto e também o contexto que eu quero testar.

Gerei o SQL, importei ele pro projeto de database:

image

E tenho o schema lá:

image

Eu quero ter uma carga de dados, então criei um plano de geração de dados, ou data generation plan:

image

Ele gera dados para minhas tabelas, vou precisar disso mais pra frente.

Criei um projeto de testes, e pra ganhar tempo, pedi pra criar um teste de banco de dados, que vai me ajudar na criação da infra de testes com banco:

image

Ele me dá a opção de gerar meu banco (fecha azul), e incluir o meu plano de geração de dados (flecha vermelha).

image

Ao fazer isso, vários artefatos interessantes apareceram no meu projeto. Vamos vê-los. Primeiro meu web.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="DatabaseUnitTesting" type="Microsoft.Data.Schema.UnitTesting.Configuration.DatabaseUnitTestingSection, Microsoft.Data.Schema.UnitTesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </configSections>
    <DatabaseUnitTesting>
        <DatabaseDeployment DatabaseProjectFileName="..\..\..\Database1\Database1.dbproj"
            Configuration="Debug" />
        <DataGeneration DataGenerationFileName="..\..\..\Database1\Data Generation Plans\DataGenerationPlan1.dgen"
            ClearDatabase="true" />
        <ExecutionContext Provider="System.Data.SqlClient" ConnectionString="Data Source=.\sqlexpress;Initial Catalog=TesteIntegradoBanco;Integrated Security=True;Pooling=False"
            CommandTimeout="30" />
        <PrivilegedContext Provider="System.Data.SqlClient" ConnectionString="Data Source=.\sqlexpress;Initial Catalog=TesteIntegradoBanco;Integrated Security=True;Pooling=False"
            CommandTimeout="30" />
    </DatabaseUnitTesting>
</configuration>

O Web.config está com toda a configuração para gerar o banco e fazer o deploy dos dados, inclusive o ponteiro para o data generation plan.

Foi criada também esta classe:

[TestClass]
public class DatabaseSetup
{
    [AssemblyInitialize]
    public static void InitializeAssembly(TestContext ctx)
    {
        DatabaseTestClass.TestService.DeployDatabaseProject();
        DatabaseTestClass.TestService.GenerateData();
    }

}

Notem o atributo AssemblyInitialize. Ele indica que quando antes deste assembly de teste rodar, este método será executado. Ele fará duas coisas: subirá o schema do banco de dados (método DeployDatabaseProject) e gerará os dados (método GenerateData). Esse segunda chamada de método não vai ficar aí, mas por enquanto tudo bem.

Agora eu posso excluir o teste de banco de dados que foi criado, ele servia pra criar os artefatos.

Agora só falta criar o teste que testa a interação com o banco em si. Vamos adicionar a classe de testes, usando o novo basic test, que vem sem aquele monte de linhas que eu sempre apago:

image

Ela fica assim:

[TestClass]
public class TesteInclusao
{
    private Produto _produtoParaIncluir;
    private Model1Container _container;

    [TestInitialize]
    public void Initialize()
    {
        Arrange();

        Act();
    }

    private void Arrange()
    {
        _container = new Model1Container();
        _produtoParaIncluir = new Produto
                       {
                           Nome = "um"
                       };
    }

    private void Act()
    {
        _container.AddToProdutoes(_produtoParaIncluir);
        _container.SaveChanges();
    }

    [TestMethod]
    public void NaoEhIgualAZero()
    {
        Assert.AreNotEqual(0, _produtoParaIncluir.Id);
    }

    [TestMethod]
    public void EstaNoBD()
    {
        var outroContainer = new Model1Container();
        var produtoIncluido = outroContainer.Produtoes.Single(p => p.Id == _produtoParaIncluir.Id);
        Assert.AreEqual("um", produtoIncluido.Nome);
    }
}

Notem que estou testando diretamente o container do Entity Framework.

O teste deve passar. Não esqueça de puxar as strings de conexão do projeto de class library para o projeto de testes, senão o container não vai ser criado. O banco subiu, o dado foi inserido. Você pode realizar uma consulta manual e confirmar, se você não acreditar no seu teste.

E um teste para excluir? Será que fuciona? Vamos olhar os dados gerados no plano de geração de dados:

image

Posso pedir para consultar por id, ou por nome, e então excluir. Vou pedir por id, que é gerado automaticamente por identity no SQL Server. Vejam o teste:

[TestClass]
public class TesteExclusao
{
    private const int IdParaExcluir = 1;
    private Model1Container _container;

    [TestInitialize]
    public void Initialize()
    {
        Arrange();

        Act();
    }

    private void Arrange()
    {
        _container = new Model1Container();
    }

    private void Act()
    {
        var produtoParaExcluir = _container.Produtoes.Single(p => p.Id == IdParaExcluir);
        _container.DeleteObject(produtoParaExcluir);
        _container.SaveChanges();
    }

    [TestMethod]
    public void NaoEstaNoBD()
    {
        var outroContainer = new Model1Container();
        var produtoExcluido = outroContainer.Produtoes.SingleOrDefault(p => p.Id == IdParaExcluir);
        Assert.IsNull(produtoExcluido);
    }
}

E funciona!

Se em outro teste eu quiser consultar o id 1, eu poderia ter problemas, já que o teste de exclusão poderia interferir. Para resolver isso eu puxo para o teste de consulta o setup dos dados no banco, veja a chamada de GenerateData, destacada.

[TestClass]
public class TesteConsulta
{
    private const int IdParaConsultar = 1;
    private Model1Container _container;
    private Produto _produtoEncontrado;

    [TestInitialize]
    public void Initialize()
    {
        Arrange();

        Act();
    }

    private void Arrange()
    {
        _container = new Model1Container();
        DatabaseTestClass.TestService.GenerateData();
    }

    private void Act()
    {
        _produtoEncontrado = _container.Produtoes.SingleOrDefault(p => p.Id == IdParaConsultar);
    }

    [TestMethod]
    public void ExisteNoBD()
    {
        Assert.IsNotNull(_produtoEncontrado);
    }
    [TestMethod]
    public void TemNomeCorreto()
    {
        Assert.AreEqual("aquele dado gigante gerado", _produtoEncontrado.Nome);
    }
}

Isso garante a consistência dos dados entre os testes.

Com isso posso criar testes à vontade, o banco sempre vai ter o último esquema e dados atualizado. Não preciso me preocupar com os dados, eles sempre vão estar lá. Isso é o básico dos testes integrado com banco de dados. O Visual Studio permite toda essa integração de maneira simples. Esse tipo de cenário fica especialmente importante quando não usamos ORMs, já que todo o acesso a dados é manual, e nesse caso testamos nossos DAOs, e também quando usamos POCO, onde o mapeamento é mais manual do que o que usei aqui.

No próximo vou mostrar como resolver isso sem o Data Dude, já que ele está limitado ao SQL Server e aos poucos providers extras que foram feitos até agora. E sem Entity Framework também.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.