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:
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?