Uma das minhas atuações na Lambda é prover treinamento de TDD e BDD em .NET. Após uma revisão rotineira do material, refleti sobre um paralelo entre objetos fake e um conceito de programação funcional.

Dos tópicos mais difíceis de se entender sobre testes de unidade é o uso de objetos fake, como mocks e stubs. Falando de formas gerais, é possível afirmar que objetos fake são como “dublês” dos objetos reais, visando substituir comportamentos específicos de acordo com o contexto. Porém, a pergunta que sempre paira: por que fazer isso?

Via de regra, usamos objetos fake para simular comportamentos incertos e/ou complicados, como um banco de dados, web service ou até mesmo sistemas de arquivos – e, para cada contexto, simulamos um determinado comportamento de forma certeira. Tal fato é incerto quando usamos objetos reais: redes podem estar desligadas, web services podem estar com schemas imprevistos, arquivos podem não ser encontrados no sistema de arquivos. Aliás, quando os objetos reais se comportam de forma esperada, não há garantias quando ao tempo de execução também, dificultando muito a escrita de testes de unidade, pois eles devem ser rápidos para serem executados constantemente.

Podemos concluir que não temos como saber o que vai acontecer toda vez que chamamos um objeto real que tem dependências externas, independentemente dos parâmetros de entrada. Ou seja, a expressão em questão não é determinística, não tendo transparência referencial – um conceito importantíssimo em programação funcional quando tange imutabilidade. Transparência referencial se resume em ter expressões determinísticas quanto à seu resultado: obter sempre a mesma resposta, dado o mesmo parâmetro – e isso não é possível com as dependências externas que citamos.

Falando em código, um exemplo canônico interessante é com NHibernate. Sua API é bem coberta por interfaces, permitindo a criação de objetos fake para simular comportamentos:

    public class ExemploNHibernate
    {
        [Fact]
        public void FingindoComNHibernate()
        {
            var sessionMockDef = new Mock<ISession>();
            sessionMockDef.Setup(x => x.Save("vai salvar")).Returns("salvou");

            var sessionFactoryMockDef = new Mock<ISessionFactory>();
            sessionFactoryMockDef.Setup(x => x.OpenSession()).Returns(sessionMockDef.Object);

            var repo = new Repository<string>();
            repo.SessionFactory = sessionFactoryMockDef.Object;
            var resultado = repo.Add("vai salvar");

            Assert.Equal("salvou", resultado);
        }
    }

    public class Repository<T>
    {
        public ISessionFactory SessionFactory { get; set; }

        public T Add(T obj)
        {
            using (var session = SessionFactory.OpenSession())
            {
                return (T)session.Save(obj);
            }
        }
    }

É justamente o ponto em que objetos fake são úteis: ao definir comportamentos específicos nos mocks e stubs que criamos, determinamos um comportamento específico que será realizado no contexto que definirmos. Notou o verbo “determinamos”? Pois bem, simulamos uma situação determinística num contexto outrora não-determinístico, assim garantindo situações que tornam os testes de unidade mais rápidos e eficientes ao isolar nossa lógica de situações externas.

Vinicius Hana