Essa é uma continuação do post anterior:
Testando o banco de dados: com Infra Microsoft
Nesse vou apresentar a mesma solução, mas usando outras ferramentas, quase todas open source. Por isso eu quero dizer tudo mesmo, só vou manter o Visual Studio de padrão.
Vamos ao cardápio:
- Para test framework: Gallio/MbUnit
- Para test runners: TestDriven.Net e Gallio/MbUnit
- Teste de banco de dados: NDbUnit
- OR/M: NHibernate
Todo o projeto está no BitBucket para download. Você pode baixá-lo aqui e vê-lo online aqui.
Vamos começar pelos testes. Vou continuar no estilo AAA para testar:
using ClassLibrary1; using NHibernate; using MbUnit.Framework; namespace TestProject1.Testes { [TestFixture] public class TesteConsulta : TesteBase { private const int IdParaConsultar = 1; private ISession _session; private Produto _produtoEncontrado; [SetUp] public void Initialize() { Arrange(); Act(); } private void Arrange() { _session = Contexto.SessionFactory.OpenSession(); } private void Act() { _produtoEncontrado = _session.Get<Produto>(IdParaConsultar); } [Test] public void ExisteNoBD() { Assert.IsNotNull(_produtoEncontrado); } [Test] public void TemNomeCorreto() { Assert.AreEqual("meu nome", _produtoEncontrado.Nome); } } }
O primeiro a notar é que estamos usando MbUnit, como framework de testes. Ele é o responsável pelos atributos TestFixture na classe e Setup e Test nos métodos. O Gallio é parte do MbUnit (ou vice versa) e roda os testes. Ao rodar este é o resultado:
Esse resultado é obtido rodando com o TestDriven.Net. Com ele, posso clicar em qualquer lugar e rodar qualquer teste. Fica muito fácil, e como ele é especializado em rodar testes unitários, ele roda um pouco mais rápido que o runner do MSTest. Veja como rodar com TDD.Net na classe:
Mais fácil impossível, né? E dá para rodar também via Powershell:
Rodando:
Terminou de rodar, resultados:
O resultado é apresentado também com um relatório HTML. Publiquei o relatório de uma rodada completa de testes aqui no blog se alguém quiser dar uma olhada no resultado. O legal é que dá pra ver até os sqls gerados pelo NHibernate.
Se rodarmos todos os testes, o resultado é esse:
E para isso, basta clicar no projeto e mandar rodar:
Ok, chega de telinhas, vamos voltar para o teste.
Estamos usando NHibernate, e a sessão do NH (análoga ao contexto do Entity Framework), é aberta via SessionFactory, que está armazenada em uma classe chamada contexto, em uma propriedade estática:
public class Contexto { public static ISessionFactory SessionFactory { get; set; } }
Esse código seria usado também pela aplicação real, além dos testes. Estamos só reaproveitando. Logo mais vou mostrar como essa variável é configurada.
O método de ação bate no NH, que bate no banco, e obtem o produto com Id igual a 1. Esse é o método Act.
Os testes são verificações simples da existência do produto, e de sua propriedade Nome.
Tenho testes para exclusão e inclusão também. Esse é o de exclusão:
[TestFixture] public class TesteExclusao : TesteBase { private const int IdParaExcluir = 1; private ISession _session; [SetUp] public void Initialize() { Arrange(); Act(); } private void Arrange() { _session = Contexto.SessionFactory.OpenSession(); } private void Act() { var produtoParaExcluir = _session.Get<Produto>(IdParaExcluir); using (var tran = _session.BeginTransaction()) { _session.Delete(produtoParaExcluir); tran.Commit(); } } [Test] public void NaoEstaNoBD() { _session.Clear(); var produtoExcluido = _session.Get<Produto>(IdParaExcluir); Assert.IsNull(produtoExcluido); } }
Neste eu excluo o Id igual a 1, e depois testo se o produto foi removido do banco, limpando a sessão para evitar cache, e fazendo novamente a consulta.
Posso rodar este teste quantas vezes quiser. Como ele continua funcionando?
Vocês já devem ter notado que os testes herdam de TesteBase. Vamos ver esta classe:
public abstract class TesteBase { [SetUp] public virtual void TestInitialize() { OperacoesDeTestes.CarregarBancoDeDados(ConfiguracaoDeTestes.Esquema, ConfiguracaoDeTestes.DadosDeTeste); } }
Notaram que ela faz uma carga do banco de dados, sempre antes de um teste rodar? É assim que garanto que o teste de exclusão nunca vai falhar por não encontrar produto para excluir, ou que o teste de inclusão vai falhar ao consultar e receber nulo.
Esta classe colabora com a classe OperacoesDeTestes. Esta classe, na prática, é só um wrapper em volta do NDbUnit. O NDbUnit é responsável por subir os dados no banco de dados, e ele também pode baixá-los. Ele faz isso com xml e xsd. Aqui vocês vêem os métodos desta classe:
public static class OperacoesDeTestes { public static void CarregarBancoDeDados( string esquema, string dados) { var baseDeDados = ObterBaseDeDados(); baseDeDados.ReadXmlSchema(esquema); baseDeDados.ReadXml(dados); baseDeDados.PerformDbOperation(DbOperationFlag.CleanInsertIdentity); } private static INDbUnitTest ObterBaseDeDados() { var baseDeDados = new SqlDbUnitTest(ConfiguracaoDeTestes.StringDeConexao); return baseDeDados; } }
Notem que o método CarregarBancoDeDados lê o schema e o xml, e salva no banco, limpando os campos identity, se existirem, sempre usando o NDbUnit.
E de onde vem os dados? Há uma classe chamada ConfiguracaoDeTestes. Vejam ela:
public class ConfiguracaoDeTestes { public static string DiretorioDeDados; public static string DadosDeBackup; public static string DadosDeTeste; public static string Esquema; public static string StringDeConexao; public static void InicializarVariaveisDeTeste( string basedir, string stringDeConexao) { StringDeConexao = stringDeConexao; DiretorioDeDados = Path.Combine(basedir, @"Dados\"); DadosDeBackup = Path.Combine(DiretorioDeDados, "DadosBackup.xml"); DadosDeTeste = Path.Combine(DiretorioDeDados, "Dados.xml"); Esquema = Path.Combine(DiretorioDeDados, "Schema.xsd"); } }
Na prática ela somente segura valores de configuração, para que possam ser acessados facilmente de qualquer lugar. Entre os dados estão os diretórios onde guardamos os arquivos xml e xsd. Esta classe recebe seus valores de outra, chamada AssemblyInitialize.
Todo bom framework de testes permite fazer um setup inicial, que roda antes de todos os testes. O MbUnit não é diferente. Para permitir isso, basta usar o atributo AssemblyFixture sobre uma classe, e usar os métodos Setup e TearDown para preparar o ambiente de testes e desmontá-lo, respectivamente.
No meu caso, na inicialização do teste, preciso inicializar as variáveis de teste, que ficam na classe de configuração vista anteriormente, preciso configurar o NHibernate, e atualizar o esquema do banco de dados. Ufa, vamos ver como fica:
[AssemblyFixture] public class AssemblyInitializer { [SetUp] public static void AssemblyInitialize() { var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); ConfiguracaoDeTestes.InicializarVariaveisDeTeste(baseDir, StringDeConexao); ConfigurarNHibernateESubirEsquemaDoBancoDeDadosDeDominio(); }
Aqui estamos declarando a classe e o método de inicialização. Estou obtendo o caminho do assembly em execução. É lá que fica meu xml e meu xsd. E passo isso, mais a string de conexão, para configurar o ambiente. Em seguida, configura o NH e subo o esquema do banco de dados.
Não vou entrar em detalhes de como o NH sobe o banco de dados, você pode ver a classe de configuração lá no BitBucket. Na prática a operação se chama schema update, onde ele compara o banco que está em produção com o que ele tem, e atualiza tudo.
Quero mostrar aqui a parte do NbUnit. Eu configuro ele com um xsd, criado a partir de um dataset tipado. Sim! Um dataset tipado. Eles ainda existem! Esse é o único lugar onde eu tenho usado datasets tipados nos últimos anos, e faz total sentido. Vejam ele, chamado de schema.xsd, aqui:
Notem três coisas. Primeiro: arranquei os table adapters, não precisamos dele, só queremos o xsd. Como queremos só o xsd, apaguei a custom tool, que fica na janela de propriedades. Sem custom tool, não há geração do arquivo que gera o dataset, isso elimina a criação do arquivo Schema.cs.
Isso me dá o schema que preciso para preparar o banco de dados. Basta então digitar o xml. O xml é muito simples:
<?xml version="1.0" encoding="utf-8" ?> <Schema xmlns="http://tempuri.org/Schema.xsd"> <Produto> <Id>1</Id> <Nome>meu nome</Nome> </Produto> <ProdutoId> <NextHi>1</NextHi> </ProdutoId> </Schema>
Usando o namespace correto, ele funciona perfeitamente e o Visual Studio ainda valida o xml enquanto você escreve.
Basta então, apenas configurar estes dois arquivos para serem copiados para o diretório de testes.
Ao rodar o método OperacoesDeTestes.CarregarBancoDeDados, e passar este xml e xsd como parâmetros, tudo funciona. Os dados vão parar no banco de dados conforme o esperado.
E com isso fecho o ciclo completo: atualizo o schema do banco, subo os dados, e testo. A partir da criação desta pequena infra, é só escrever os testes livremente, sempre herdando de TesteBase.
O que acharam? Qual preferem, com infra Microsoft, ou infra (semi)open? Porque?
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.