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