Criando e testando objetos com construtores privados
Imaginem a seguinte situação. Temos uma classe qualquer. Essa classe possui uma dependência. Além disso, vamos supor que a criação desse objeto seja uma coisa complexa, ou seja, não podemos simplesmente deixar que essa classe seja instanciada por um new.
// MeuObjeto.cs
public class MeuObjeto
{
public IMinhaDependencia Dependencia { get; set; }
internal MeuObjeto()
{
// internal para garantir que apenas
// a factory possa criar este objeto
}
public int RetornarCalculo(int a)
{
return Dependencia.Calcular() + a;
}
}
// IMinhaDependencia.cs
public interface IMinhaDependencia
{
int Calcular();
}
Temos então uma classe factory para criar uma instância desse objeto. Problema resolvido, correto? Se eu precisar de uma instância dessa classe, simplesmente chamo sua factory.
// MeuObjetoFactory.cs
public class MeuObjetoFactory
{
public MeuObjeto CriarObjeto()
{
return new MeuObjeto()
{
Dependencia = new MinhaDependencia()
};
}
}
// MinhaDependencia .cs
// faz de conta que é um objeto muito custoso para criar
public class MinhaDependencia : IMinhaDependencia
{
public int Calcular()
{
return 1;
}
}
Do ponto de vista funcional, funciona que é uma beleza. Mas como podemos testar unitariamente a classe MeuObjeto? Neste caso, teríamos duas opções:
- Usamos a factory no nosso teste de unidade para criar o objeto. O problema nesse caso é que nosso teste de unidade passaria a testar a classe MeuObjeto, a factory MeuObjetoFactory além de obrigatoriamente criar uma instância da dependência MinhaDependencia, o que implica que a testaríamos a sua criação também. Isso deixou de ser um teste de unidade.
- Alteramos o escopo de MeuObjeto e deixamos seu construtor público. Com isso, podemos instanciá-lo diretamente, sem usar nada do factory nem criar sua dependência, e passar um objeto dublê de teste (mock) para simular o comportamento da dependência. Só que com isso, qualquer desavisado poderia instanciar diretamente MeuObjeto, e era exatamente isso que queríamos evitar quando fizemos seu construtor com escopo internal (visível apenas dentro do assembly que ele reside) e criamos a factory.
E agora, qual opção seguir?
Bom, temos uma terceira opção, pegando o que há de bom em cada cenário acima, sem suas desvantagens!
Vamos usar uma classe chamada PrivateObject, disponível no namespace Microsoft.VisualStudio.TestTools.UnitTesting. Com ela, podemos criar e chamar métodos de qualquer classe, mesmo que esses métodos sejam privados. Olhem só a classe de testes abaixo.
// UnitTest1.cs
[TestClass]
public class UnitTest1
{
[TestMethod]
[Ignore]
public void TestMethod1()
{
//var obj = new MeuObjeto(); -- isso dá erro de compilação
}
[TestMethod]
public void TestMethod2()
{
// Arrange
var mock = new Mock<IMinhaDependencia>();
mock.Setup(m => m.Calcular()).Returns(1);
var po = new PrivateObject(typeof(MeuObjeto));
var obj = (MeuObjeto)po.Target;
obj.Dependencia = mock.Object;
// Act
var resultado = obj.RetornarCalculo(2);
// Assert
Assert.AreEqual(3, resultado);
}
}
O método TestMethod1 é só para mostrar que não funciona instanciar direto um objeto com construtor não visível (privado ou como no nosso caso, internal). Vamos olhar o método TestMethod2, que é mais interessante para nós. A primeira coisa que ele está fazendo é criar um objeto dublê de teste usando o framework Moq. Não é escopo deste post explicar mocks ou o Moq, então aqui está um link para mais informações http://code.google.com/p/moq/wiki/QuickStart. Mas explicando de forma bem rápida, o que estamos fazendo é criar um objeto que possua os mesmos métodos da interface IMinhaDependencia, e ajustando o comportamento que ele terá quando formos chamar o seu método Calcular (no nosso exemplo, estamos cravando que seu retorno será o valor 1). Com isso, "enganamos" a instância de MeuObjeto, fazendo com que ele acredite que esteja trabalhando com uma implementação concreta.
Bom, voltando ao assunto original, chegamos agora na parte onde efetivamente criamos o objeto de construtor não-visível. Simplesmente criamos uma instância de PrivateObject passando o tipo que queremos que ele crie. De forma "mágica", ele nos cria esse objeto (internamente ele deve usar .NET Reflection para criar a instância, mas como eu não tenho certeza disso, considero que é por mágica). Assim, conseguimos pegar a sua referência acessando a propriedade Target de PrivateObject. Com isso, podemos continuar nosso teste de unidade, passando o mock que foi gerado e testando a sua funcionalidade dentro do teste de caixa-branca.
Por hoje é só. Em breve eu disponibilizo os fontes desse exemplo em algum repositório público (ainda estou decidindo se eu uso o BitBucket ou o GitHub).
[]'s
ATUALIZAÇÃO: Disponibilizei o exemplo em https://github.com/fabiogouw/Exemplos. Decidi deixar minha conta no Github para coisas públicas, e o Bitbucket para coisas pessoais.
ATUALIZAÇÃO: Disponibilizei o exemplo em https://github.com/fabiogouw/Exemplos. Decidi deixar minha conta no Github para coisas públicas, e o Bitbucket para coisas pessoais.
Comentários
Postar um comentário