Este é um tópico no qual estou muito interessado. Há muitos puristas que dizem que você não deve testar tecnologias como EF e NHibernate. Eles estão certos, eles já foram rigorosamente testados e, como uma resposta anterior afirmou, muitas vezes não faz sentido gastar muito tempo testando o que você não possui.
No entanto, você possui o banco de dados abaixo! É aqui que, na minha opinião, essa abordagem é quebrada, não é necessário testar se a EF / NH está fazendo o trabalho corretamente. Você precisa testar se seus mapeamentos / implementações estão funcionando com seu banco de dados. Na minha opinião, essa é uma das partes mais importantes de um sistema que você pode testar.
A rigor, porém, estamos saindo do domínio do teste de unidade para o teste de integração, mas os princípios permanecem os mesmos.
A primeira coisa que você precisa fazer é poder zombar do seu DAL para que seu BLL possa ser testado independentemente do EF e do SQL. Estes são os seus testes de unidade. Em seguida, você precisa projetar seus testes de integração para provar seu DAL. Na minha opinião, eles são igualmente importantes.
Há algumas coisas a considerar:
- Seu banco de dados precisa estar em um estado conhecido a cada teste. A maioria dos sistemas usa um backup ou cria scripts para isso.
- Cada teste deve ser repetível
- Cada teste deve ser atômico
Existem duas abordagens principais para configurar seu banco de dados, a primeira é executar um script de criação de banco de dados do UnitTest. Isso garante que seu banco de dados de teste de unidade esteja sempre no mesmo estado no início de cada teste (você pode redefinir isso ou executar cada teste em uma transação para garantir isso).
Sua outra opção é o que eu faço, execute configurações específicas para cada teste individual. Eu acredito que esta é a melhor abordagem por duas razões principais:
- Seu banco de dados é mais simples, você não precisa de um esquema inteiro para cada teste
- Cada teste é mais seguro, se você alterar um valor no script de criação, ele não invalidará dezenas de outros testes.
Infelizmente, seu compromisso aqui é a velocidade. Leva tempo para executar todos esses testes, para executar todos esses scripts de configuração / desmontagem.
Um ponto final, pode ser um trabalho muito difícil escrever uma quantidade tão grande de SQL para testar seu ORM. É aqui que tomo uma abordagem muito desagradável (os puristas aqui vão discordar de mim). Eu uso meu ORM para criar meu teste! Em vez de ter um script separado para cada teste de DAL no meu sistema, tenho uma fase de configuração de teste que cria os objetos, os anexa ao contexto e os salva. Eu então executo meu teste.
Isso está longe de ser a solução ideal, no entanto, na prática, acho muito mais fácil gerenciar (especialmente quando você tem vários milhares de testes); caso contrário, você está criando um grande número de scripts. Praticidade sobre pureza.
Sem dúvida, analisarei essa resposta em alguns anos (meses / dias) e discordarei de mim mesmo à medida que minhas abordagens mudaram - no entanto, essa é minha abordagem atual.
Para tentar resumir tudo o que eu disse acima, este é meu teste de integração de banco de dados típico:
[Test]
public void LoadUser()
{
this.RunTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
return user.UserID;
}, id => // the ID of the entity we need to load
{
var user = LoadMyUser(id); // load the entity
Assert.AreEqual("Mr", user.Title); // test your properties
Assert.AreEqual("Joe", user.Firstname);
Assert.AreEqual("Bloggs", user.Lastname);
}
}
O principal a notar aqui é que as sessões dos dois loops são completamente independentes. Na sua implementação do RunTest, você deve garantir que o contexto seja confirmado e destruído e seus dados só possam vir do seu banco de dados para a segunda parte.
Editar 13/10/2014
Eu disse que provavelmente revisaria esse modelo nos próximos meses. Embora eu mantenha a abordagem que defendi acima, atualizei levemente meu mecanismo de teste. Agora, tenho a tendência de criar as entidades no TestSetup e TestTearDown.
[SetUp]
public void Setup()
{
this.SetupTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
});
}
[TearDown]
public void TearDown()
{
this.TearDownDatabase();
}
Em seguida, teste cada propriedade individualmente
[Test]
public void TestTitle()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Mr", user.Title);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Bloggs", user.Lastname);
}
Existem várias razões para essa abordagem:
- Não há chamadas adicionais ao banco de dados (uma configuração, uma desmontagem)
- Os testes são muito mais granulares, cada teste verifica uma propriedade
- A lógica Setup / TearDown é removida dos próprios métodos de teste
Eu sinto que isso torna a classe de teste mais simples e os testes mais granulares ( declarações simples são boas )
Editar 03/05/2015
Outra revisão sobre essa abordagem. Embora as configurações em nível de classe sejam muito úteis para testes, como carregar propriedades, elas são menos úteis quando são necessárias diferentes configurações. Nesse caso, a criação de uma nova classe para cada caso é um exagero.
Para ajudar com isso, agora tenho duas classes base SetupPerTest
e SingleSetup
. Essas duas classes expõem a estrutura conforme necessário.
No SingleSetup
temos um mecanismo muito semelhante ao descrito na minha primeira edição. Um exemplo seria
public TestProperties : SingleSetup
{
public int UserID {get;set;}
public override DoSetup(ISession session)
{
var user = new User("Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Bloggs", user.Lastname);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
}
No entanto, referências que garantem que apenas as entidades corretas sejam carregadas podem usar uma abordagem SetupPerTest
public TestProperties : SetupPerTest
{
[Test]
public void EnsureCorrectReferenceIsLoaded()
{
int friendID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriend();
session.Save(user);
friendID = user.Friends.Single().FriendID;
} () =>
{
var user = GetUser();
Assert.AreEqual(friendID, user.Friends.Single().FriendID);
});
}
[Test]
public void EnsureOnlyCorrectFriendsAreLoaded()
{
int userID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriends(2);
var user2 = CreateUserWithFriends(5);
session.Save(user);
session.Save(user2);
userID = user.UserID;
} () =>
{
var user = GetUser(userID);
Assert.AreEqual(2, user.Friends.Count());
});
}
}
Em resumo, ambas as abordagens funcionam dependendo do que você está tentando testar.