Test-Driven Development é desnecessário!

Há alguns dias atrás ouvi a seguinte afirmativa:

Acredito que testes de unidade são importantes; Não vejo necessidade porém, em fazer TDD.

Será?

Separando as coisas: Teste de Unidade

Teste de unidade, como comentei anteriormente, tem como objetivo garantir que uma parte da regra de negócio funcione como foi solicitada/descrita pelo interessado pelo software. O teste deve abranger apenas um cenário, seja ele feliz (do caminho feliz); ou falho. Todo e qualquer input que se faça necessário deverá ser introduzido com o auxílio de Mocks ou/e Stubs, fazendo assim com que o teste seja o mais independente possível do restante das classes que colaboram/interagem com aquele trecho na vida real (código de produção).

Uma maneira de verificar se seu código de teste está bem escrito é atentar-se para a anatomia do teste de unidade: deverá ter um trecho para input dos dados necessário ao teste; execução do código que está sob teste; e por fim, analisar os resultados obtidos.

Fazer o teste depois do código de produção poderá no início trazer confiabilidade do que se está fazendo. Ao crescer a aplicação porém, é provável que você perca o controle da suite e o resultado é preocupante: um bolo de testes acoplados e sem garantias sob o código de produção. Nessa hora você cairá um beco sem saída e culpará injustamente “essa falácia de testar software”.

Podemos afirmar também que fazer teste de unidade para mostrar para o chefe não é uma boa ideia, pois o parabéns que você obterá com ele hoje, poderá se transformar no maior vilão, quando você perder o controle estrutural do seu código.

Separando as coisas: Test-Driven Development

Tem seu cerne em: Red, Green, Refactor. Em miúdos:

  1. Escreva o teste que irá exercitar uma parte isolada do seu código (leia acima sobre isso).
  2. Escreva o código (de produção) que irá fazer o teste ficar verde (passar).
  3. Verifique se seu código de produção (e o de teste) podem ter melhora, tanto na escrita quando na qualidade e best practices da linguagem.

Repita esse processo até começar a colher os resultados do TDD. Depois que estiver mais acostumado (coisa de algumas semanas), você poderá se adaptar melhor. Lembre-se: Baby Steps são importantes nesta fase: faça uma coisa de cada vez. Não escreva 20 linhas de código sem ir verificando com seu teste o que está acontecendo. Não tenha pressa. Você está aprendendo uma nova forma de ver o mundo.

Outro dia, mexendo num código da galera aqui, encontrei um método que possuia 37 variáveis temporárias; 7 loops; 11 ifs/else; 5 comentários de uma linha.

É nítido que o programador não sabia mais onde colocar as coisas. Ele perdeu o controle do código de uma forma tão intensa, que ele precisou fazer 3 loops com a mesma Collection, para fazer coisas diferentes com ela.

Nem é preciso entrar no detalhe de Single Responsibility Principle; Dont Repeat Yourself. Ele não sabia aonde colocar cada coisa. Ponto.

É nisso que o TDD auxilia. Ele está ali como um guarda, pronto para levantar uma bandeira assim que você começa a se perder. Ele sinaliza através dos testes a necessidade de repensar a solução, reorganizar o código para a nova feature, para fazer o flush mental.

Test-Driven Development é sobre onde colocar seu código e não sobre Verificação e Validação.

Enquanto você não ter essa visão, continuará a sofrer com debugging, duplicação à n-ésima (duplicação da duplicação n vezes) e o renomado Bug Ioiô: bug que é resolvido e depois aparece novamente.

O ponto de encontro

Teste de Unidade + Design Orientado a Objetos: Test-Driven Development Design.

Concluindo

Não, você não é o Jiraya da programação. Pouco importa se você tem toda a ideia na sua cabeça, aposto com você de que, se ao invés de primeiro fazer o código de produção fizer o teste, aquela sua toda ideia inicial terá sido apenas um rascunho da versão que criará guiada pelos testes.

A dica final é: não seja afobado. Ao começar fazer testes de unidades, faça do modo clássico: Red, Green, Refactor.

Tem uma definição bacana sobre Test-Driven Development? Mande um tweet com a hashtag #deftdd ou me mencione. Vou retuitar todas as criativas 😉

Unit Testing em aplicações já existentes e sem teste

Se você achou interessante começar uma aplicação com Test-Driven Development, o que dizer sobre um dos fatores mais comuns em nossa área que é assumir um projeto que já está em produção, porém não possui testes de unidade ?

Softwares legados também possuem direito de ter seu lugar ao sol !

Cenário clássico

Você entra para uma nova equipe de desenvolvimento. Como um tester acostumado com as práticas do Test-First, você vai cegamente à procura do diretório(s) com os testes para: executá-los afim de descobrir se configurou o ambiente corretamente; ler, afinal quer entender um pouco mais do projeto; depois de vasculhar /tests, /spec, /xpto/namespace/module/tests e não encontrar nada você descobre que: o projeto não possui testes!

Normal? Infelizmente sim – Há muitos desenvolvedores que não gostam de testes. Os motivos variam e é assunto futuro -, vários projetos que vi não possuiam testes at all. Em todas às vezes porém, meus gestores eram favoráveis a adoção e receptivos a mudança de comportamento que isto causaria.

Sem testes e com carta branca para começar a mudar o cenário. O que fazer agora ?

$ ./configure

Antes de tudo, você precisa ver como o projeto está feito. Passe umas horas ou dia olhando o que tem ali e por onde poderia começar a trabalhar. Após isto, vamos listar coisas importantes sobre o código que já funciona em produção:

  • Não faça testes de unidade do que já existe;
  • Não seja convencido a fazer testes de unidade do código existente;
  • É insanidade querer testar as partes de um código que já funciona. Não faça isso;
  • Não faça teste de unidade do código existente.

Motivo para todos itens acima é o mesmo: teste de unidade é seu guia para construir o software requisitado da menor e melhor forma possível. Ele é um guia e não uma Bug Tool. Se o código já está presente, você, se cair na tentação de fazer teste daquilo, forçará seu teste a passar naquele código. Seu teste pode até pegar um bug, mas você, novato no projeto, não saberá que é um bug e acabará mudando o teste para passar naquela condição bugada. Costumo dizer que testar código já feito é perder tempo.

Vou comentar dois cenários aqui: a) você precisa criar um módulo/integração/package novo dentro desse projeto legado; b) você precisa adicionar testes ao código existente.

Vamos lá 🙂

$ make all

Vou pegar os dois casos e comentários sem separado:

a) Você precisa criar um módulo/integração/package novo dentro desse projeto legado

Para mim, dentre os dois esse é o mais fácil. Tudo que você precisa fazer é começar o módulo novo de forma isolada. Ou seja: evite depender do código sem teste dentro do seu módulo novo. Você pode obter isso com o uso de Adapters. Crie seu módulo novo, seguindo Test-Driven Development e toda vez que precisar da colaboração de uma classe/objeto que não tem o devido teste, crie um Adaptador que isolará aquela dependência instável do seu código novo. Isso te dará maior confiabilidade no que está fazendo.

Isole toda a comunicação do seu módulo novo, com o uso de Façades.

Supondo que você está fazendo um módulo de pagamento, crie uma façade para seu código e a faça prover os recursos que o restante do software necessitar:

    class PaymentFacade {
        public ... create(...);
        public ... createRecurrent(...);
        public ... getPayments(Datetime forDate);
        public ... cancelRecurrent(Integer paymentId);
        public ... processPayment(Integer paymentId);
        ....
    }

Na sua Façade, trafegue valores escalares e não objetos do sistema. Isto tornará seu module de pagamento mais isolado e protegido do código não testado.

b) Você precisa adicionar testes ao código existente

Lembre-se: não adicione testes de unidade ao código já existente e sem teste. Partindo disto, seguimos:

Anticorruption Layer, DDD

Este é mais complicado. Costumo utilizar uma técnica do livro Domain-Driven Design de Eric Evans chamada Anti-corruption Layer.

A ideia aqui é fazer igual na figura: o “ACL” é o Anti Corruption Layer, a camada que irá defender seu código novo do código legado sem testes.

Dando um zoom nesta imagem, tudo que precisamos fazer é criar uma layer que conterá classes/objetos ou/e métodos em classes existentes que farão adapters/façades, traduzindo o que o sistema já tem para conseguirmos encaixar no que estamos fazendo.

DDD Zoom-in

Client System: seu sistema legado
Anticorruption Layer: o meio entre seu sistema legado e seu módulo novo
External: seu código novo, totalmente Test-First.

Não sinta-se intimidado em tentar fazer deploy de uma versão funcional com seus adapters, façades e anticorruption layer. Eu já mantive em produção por algumas semanas código “macabro”, cheio de “fios pendurados” e coisas em aberto, até que eu pudesse ajustá-las para em deploys futuros, pudesse removê-los tranquilamente.

# make install

Berlim Wall, the fall

Agora que consegue deployar seu código novo e testado junto do software que tinha antes sem testes de uma forma até que harmoniosa e isolada, você precisa derrubar o muro de Berlim que construiu: você precisará refatorar seu código legado e sem testes para que comece a adicionar testes nele também. Ao adicionar testes no código refeito, você poderá remover os Adapters, Façades e até a Anticorruption Layer daquele pedaço que isolava o código sem testes do código “novo”.

Isto é uma parte importante, pois você precisará fazer tudo de dentro para fora. Explico: fora é a parte mais próxima do seu cliente possível (Controller, API, etc); dentro é a parte mais próxima do código que está isolando/refatorando. Sempre venha de dentro para fora nas tuas refatorações, pela razão de que assim, você afetará a menor quantidade possível de classes, pois identificará logo de pronto até onde pretende refatorar, colocando logo na sequencia disto seus adapters/façades.

Concluindo

Agora você tem um bom motivo para utilizar adapters e façades. Esse tipo de refatoração é uma das minhas favoritas, pois envolve muito de lidar com isolamentos bem construídos e como manter isto de forma que não quebrará todo o software. O resultado final é uma aplicação funcional com um toque de código novo e testado ao mesmo tempo.

Test-First: a anatomia de um teste

Agora que as devidas apresentações foram feitas, você já deve estar inteirado O que o Test-Driven Development não é; O que são Testes de Unidade e até como conseguir criar seu primeiro teste. Assim sendo, acredito que é possível partirmos para conteúdos mais específicos dentro de cada assunto.

Para começar, vou apresentar o que acredito ser uma anatomia válida para um teste de unidade.

Ana….tomia ?

Uma pergunta até que comum que alguns co-workers têm feito é: como identificar se meu teste está bem construído ?

Essa pergunta é um resumo das seguintes perguntas:

  • (Até) Quantas linhas deve ter um teste de unidade ?
  • Já li que é assertion por teste. Não pode ter mais por quê ?
  • Como consigo testar se um Adapter que formata dados para JSON retorna valores fidedignos, se eu tenho √784 itens no retorno ? Fiz um assertion para cada key. Não posso ficar sem testar isso. Tem ideia melhor ?

Para conseguir mostrar visualmente como isso é possível, temos que adicionar ao nosso In-Memory Testing Toolbox a técnica de como identificar se um teste tem partes faltantes (ou excedentes).

Um Teste de Unidade – teste no geral – pode ser separado em três partes: inicialização de dados; exercitar o código de produção; e, analisar resultados. Let’s turn it visible:

    public void testSomaDoisValoresDistintos() {
        int valorA = 10;
        int valorB = 20;
        float resultado = subject.somaValores(valorA, valorB);

        assertEquals(30, resultado, 0.0);
    }

Identificando as três partes:

Inicialização de dados:

int valorA = 10;
int valorB = 20;

Exercitar o Código de Produção:

float resultado = subject.somaValores(valorA, valorB);

Analisar Resultados:

assertEquals(30, resultado, 0.0);

Acredito que essa regra sirva muito bem para você se policiar sempre ao fazer seus testes de unidade. Isso quer dizer: você não precisa fazer 1 assertion/teste; Não precisa sempre criar teste com três linhas (exceto quando estiver treinando – assunto futuro(..)).

Isso quer dizer que não tem nada errado em ter aqueles √784 (28) assertEquals no meu teste do Adapter JSON, certo?

Errrr…. errado! Teoricamente falando isso é possível, porém é importante lembrar que frameworks de testes deste século utilizam técnicas de Hotspoting (assunto p/ outro post) que permitem nós definirmos nossos próprios assertions. Já visualizou como resolver o problema do JSON?

    public void testJsonAdapterReturnsEncodedJsonString() {
        # ... inicializa seu objeto para popular dados
        assertJsonAdapterWillContainKeysValues(subject.toJson());
    }

Mesmo sendo uma forma mais compacta, ainda assim temos a inicialização do teste, o exercício do código de produção a ser testado e a validação de que aquele código está de acordo com a especificação.

Mas… porque raios está errado colocar 28 assertions dentro de um teste?

Seu Teste é uma Especificação

Como disse Richard Stallman em The Code Linux, seu código é uma receita. Vamos utilizar a analogia dele em nosso favor: sendo o Teste uma receita, ele deve possuir steps que irá definir como ele chegará ao resultado final, ou seja, o Teste é uma Especificação de como aquele comportamento exercitado pelo mesmo deverá funcionar.

  • Coloque 400ml de água dentro de uma panela e leve-a ao fogo médio;
  • Espere 5 minutos para esquentar;
  • Abra o pacote do miojo e o coloque dentro da panela;
  • Após 3 (tsc, tsc) minutos, adicione o tempero e misture bem.
  • Sirva.

Se conseguir enxergar aqueles três passos da anatomia do teste como uma receita, ficará mais fácil para lembrá-los e visualizá-los nos códigos alheios.

Como uma especificação, ao invés de precisar ler 28 linhas para entender que tudo aquilo é apenas o passo de análise de resultados, não acha mais fácil criar seu assertion customizado e resolver isso com apenas 1 linha – assertJsonAdapterWillContainKeysValues(resultado) ?

A Anatomia apontará mutações genéticas

Seguindo a Anatomia do Teste, se algum dos passos aparecerem mais de uma vez por teste, é indício de que seu teste está testando mais do que uma unidade. Ao topar com isto, você deve: extrair a duplicação e criar outro teste para aquele conteúdo duplicado. Em casos extremos, a duplicação não poderá ser removida. Isso indica ao coder de que a abstração dele caiu por terra ou ainda você está testando o comportamento errado. Será necessário repensar a solução daquele pedaço.

Concluindo

Adote a Anatomia do Teste como sua métrica para um teste conciso. Leia sobre como criar Custom Matchers/Assertions em seu framework favorito de testes. Pratique isso no seu tempo livre. Avalie e peça avaliação de seus códigos de Teste e Produção. Exponha-se à críticas. Fará muito bem.

Code Coverage: uma das consequências do Test-First

Quando comecei a tentar testar meus códigos, iniciei também a busca por métricas que indicassem que eu estava no caminho correto, evitando me desvirtuar durante essa mudança de pensamento ao codificar software. Na época, o manual do PHPUnit indicava que o framework de teste possuia ferramentas para cobertura de código. Eis que fui apresentado ao Code Coverage.

“Uau! Esta é uma métrica indiscutível para mensurar a qualidade de meus testes!” – pensei na época.

Depois que tomei conhecimento da análise da Cobertura de Código pelos testes, não se falava em outra coisa a não ser:

Neste projeto o objetivo é ter 90% de cobertura de código; Mas no próximo, aaaaah, no próximo nada menos do que 98% pode ser aceito.

Sério. Ficávamos numa neura assim mesmo. Não era para menos. Pense comigo: sendo como objetivo do teste, garantir que o código de produção funcione, nada mais sensato do que cobrir a maior quantidade do Código de Produção (todo código que não é de teste ou do framework no projeto, assim digamos) possível. Ou seja: a ideia é que nossos testes passe pelo maior número de linhas possíveis para garantirmos que toda linha está funcionando como deveria em seu devido fluxo. Por exemplo:

    class User {

        public void fazLalala(String comFoo) throws Exception {
            if (comFoo == null) {
                throw new Exception("Lascou!");
            }

            if (comFoo.startWith("Lalala")) {
                this.doSomething(comFoo);
            } else {
                this.doAnother(comFoo);
            }
        }
    }

No caso deveríamos exercitar os 3 possíveis casos: lançamento de exception; if == true e if != true, afinal o teste é para verificar se a exception é lançada somente quando necessário e o if/else trabalhar conforme o esperado vindo do input do método.

Porque este pensamento está incorreto

Testar as possibilidades do comportamento de um método de um objeto é fundamental, porém, no caso acima, eu estava olhando pela ótica errada.

Eu estava testando para obter Code Coverage ao invés de obter Code Coverage por estar testando.

Inversamente a Matemática, no português a inversão dos valores podem não serem equivalentes, como neste caso. Testar com o objetivo de obter Code Coverage não é um bom motivo; agora, obter um bom Code Coverage por causa do teste é algo muito bom.

Para ilustar o problema de Testar Code Coverage Driven, vamos ver o código abaixo:

    class User {

        public void addCredentials(String username, String password) {
            // do something...        
        }

        public String getName() {
            return "...";
        }

        public String getCredentials() {
            return "...";
        }

        public String getLastname() {
            return "...";
        }
    }

Ao Testar focado em obter Code Coverage, algo assim poderá ser encontrado na classe de teste do User:

    class UserTest {

        @test
        public void testGetName() {
            user = new User();
            user.setName("name");

            assertEquals("name", user.getName());
        }

        @test
        public void testGetLastname() {
            user = new User();
            user.setLastname("last name");

            assertEquals("last name", user.getLastname());
        }

        @test
        public void testAddCredentials() {
            user = new User();
            user.addCredentials("username", "senha_marota");

            assertEquals([["username", Digest::MD5.hexdigest("senha_marota")]], user.getCredentials());
        }

É possível observar dois problemas aqui:

  1. Getter/Setter foi testado isoladamente, como se fosse uma regra de domínio.
  2. O teste do addCredentials teve o mesmo tratamento do que os Getters/Setters.

Não me entenda errado: não existe nada que diga para não testarmos getters/setters. O problema é: testar coisas que serão testadas em métodos futuros por consequência. Por consequência que digo seria:

    @test
    public void testCredentialUsernameShouldNotEqualsToName() {
        String name = "Mesmo nome";
        user = new User();
        user.setName(name)
        user.addCredentials(name, "senha_marota");

        assertEmpty(user.getCredentials());
    }

Assumindo que uma das regras da Credencial é que o username não seja igual ao nome do usuário, um teste deste tipo já nos garante que o setName() e o getName() funcionam como esperamos que funcionará, pois o addCredentials irá chamar o getName para achar o nome do usuário:

    public void addCredentials(String username, String password) {
        if (getName() == username) {
            // do nothing / throws exception
        }
    }

Com isto, o teste testGetName() pode ser dispensado tranquilamente, pois ele foi testado em consequência ao teste de negócio testCredentialUsernameShouldNotEqualsToName.

O segundo problema é ainda mais grave: por testar getter/setter, você poderá cair em copy/paste nos testes pela sua similaridade e talvez isso o fará cair na armadilha de esquecer de testar o que realmente precisa de atenção por conter Regras de Negócio (Domínio) envolvida, como o addCredentials tem. Testes válidos para a classe proposta seriam: testAddNewCredential, testCredentialUsernameShouldNotEqualsToName e talvez até testDuplicatedCredentialShouldRaiseError.

Porque este pensamento está correto

A corretude de Testar Orientado a Cobertura de Código está em se e somente se o Teste fosse para garantir que o código funciona o que não é o objetivo do teste de unidade. Por não ser objetivo principal, testar para satisfazer a cobertura de código não trará garantia alguma, além de que o software não têm erros de sintaxe ou fluxo. É alguma coisa? Sim, claro! Mas estará longe da grande vantagem que Test-First traz: montar sua aplicação da forma mais orquestrada possível.

Concluindo

Foque nos testes que agregam valor ao negócio(domínio) do software que os métodos auxiliares serão avaliados em consequência, trazendo uma cobertura altíssima e mais: confiável. Exercite seu addCredentials criando um teste para cada regra de negócio que lhe foi solicitada, dando segurança real no seu Código de Produção.

Mock elevado à enésima potência

itsatrap

Mocks: mock é um assunto polêmico em Software Testing nos dias atuais. Quase todos defendem o uso, Robert Martin que o diga. Logo, você começa a ler, um passo depois está fazendo seus próprios mocks e gosta do que vê. Num piscar de olhos, está Mockando tudo que pode e fica feliz com isto.

Ver um teste com 3, 4, 5 até 6 mocks pode no começo parecer algo sensacional, mas uma coisa que é necessário aprender sobre Mocks é que assim como seu teste, ele fala. Não uma mutação do say do OS X, mas com sua própria e simples linguagem: em excesso torno-me algo extramemente ruim. É exatamente isso que ele diz aos berros quando ele é super utilizado em um mesmo Test Case.

Pecando pelo excesso

Ao Mockar, esqueça essa frase popular de que é melhor pecar pelo excesso. Overmocking é tão ruim quanto não mockar at all. Se eu tivesse que escolher entre Mockar excessivamente ou não mockar nada, eu optaria pela segunda opção mantendo todas minhas dependências levemente soltas. (assunto para posts futuros!)

Quando você estiver forçado a stubar/mockar muita coisa para que um teste seu possa ficar isolado o suficiente para então testar a funcionabilidade (que você isolou), isso quer dizer simplesmente o seguinte: seu design ficou acoplado e como tal, precisa ser repensado para algo menos atrelado. Deixando suas dependências (outras classes) mais independentes, a quantidade de mocks por teste cairá e seu problema sucumbirá.

O acoplamento é aterrorizante para alguns ou/e em algumas situações, mas eles acontecem quando se perde o controle do que está criando ou quando se modifica o comportamento sem modificar como as coisas se falam. Sabe aquela história de: “ah, modifica isso aqui rapidinho só para X fazer Y.” – quando você muda, 99.9% de chances de criar acoplamento e a consequência disso reflete diretamente sobre a quantidade de itens mockados por teste.

Mocks por Teste

Matematicamente dizendo, não há um número exato para mocks por teste (mocks/teste), mas eu tenho meu limite pessoal baseado em nada erro e acerto: 3 mocks/teste para mim é o limite aceitável. Ao ultrapassar isto, automaticamente aquele meu código que o teste exercita cairá na minha Lista de Redesign. Isto forcará a repensar se é caso de refazer ou se é uma exceção. Tudo questão de análise caso a caso. Caso você tenha uma abordagem diferente com seu limite por teste, comente ao final 😉

Mockar apenas o que te pertence

Um ponto defendido por alguns é o de only mock what you own, em outras palavras seria: não mock seu framework; não mock sua API; Mock apenas o que você criou. Aquilo que faz parte do seu domínio. Seu framework é um domínio transversal, ou seja, é um domínio que auxilia você a criar seu próprio domínio. Lembre-se disso!

Com o Rails em particular, eu acabei por várias vezes não seguindo isto. Na verdade, eu não sou muito a favor disto pode me julgar, xingar, etc.. O motivo é simples: o Active Record se mistura de uma forma tão intríseca ao seu domínio, que em alguns momentos é difícil dizer aonde está a linha que separa o framework do seu código. Por este motivo, eu já fiz mock dos finders do ActiveRecord.

O problema de mockar o framework é que você ao fazer esse tipo de maluquisse precisa estar atento é de que ao mocka-lo, você presume como ele irá se comportar dado um certo input porém se por algum motivo sua intuição/palpite estiver errado, seu teste te dará um resultado errado. Com o resultado errado, seu teste passará, mas na hora de rodar o código de produção sem os mocks, o comportamento esperado não acontecerá. O Teste te deixará na mão, por causa do mock e por falha sua.

Mockar o framework é muito perigoso. O teste poderá tornar-se arisco. O true poderá ser false e você precisará lidar com isto.

Concluindo

Mock é um aliado importantíssimo em Test-First. Sabê-lo utilizá-lo é ainda mais importante.

Definir classes não é programar com orientação a objetos: Ciclo de vida

Há pouco mais de 2 anos, falei sobre esse tema na PHP Conference Brasil e acho válido revisitá-lo uma vez que muitos desenvolvedores sentem orgulho em dizer que programam orientado a objetos, mas ao fazer uma simples pergunta acabam não sabendo respondê-la:

O que é Orientação a Objetos ?

Alan Kay, criador da linguagem Smalltalk e um dos primeiros defensores da orientação a objetos, a explicou da seguinte forma:

“The big idea is “messaging” (…)

Pronto, fim do post. Obrigado por ler.

Messaging ?

De nada adianta falar sobre Test-First, Princípios de Orientação a Objetos, Mocking, AntiPatterns uma vez que não se conhece o núcleo da Orientação a Objetos.

O grande cerne da questão é justamente a troca de mensagem entre objetos através de seus métodos. É assim que Alan Kay defendeu e é assim que o Smalltalk funciona. Recentemente, alguns desenvolvedores, especialmente em Ruby, têm voltado a reforçar o propósito em OO.

A melhor forma de manter seu código desacoplado e “falante” é forçar os objetos a trocarem mensagens. Lembra do Tell Don’t Ask? Pois é, aqui isso é parte chave para atingir-se tal objetivo.

Vou repetir:

Pronto, acho que deu para entender o quão importante é manter no objeto a responsabilidade de manipular os próprios dados (armazenados em vossos atributos).

O Ciclo de Vida de um Objeto

Já parou para pensar em qual seria o ciclo de vida de um objeto?

Bom, inicialmente ele Nasce, depois Cresce, Continua disponível e finalmente Morre.

O nascimento de um objeto

O nascimento do objeto é através de seu construtor. Simples, porém muitas vezes ele acaba ignorado, ainda mais quando vicia-se em utilizar frameworks arquiteturais sem entender o motivos das coisas.

Correto:

    Employee.new(name: "Durval", lastname: "da Silva", cpf: "123.123.123-X")

    new Employee("Durval", "da Silva", "123.123.123-X")

Incorreto:

    durval = Employee.new
    durval.name = "durval"
    durval.lastname = "da Silva"
    durval.cpf = "123.123.123-X"

    durval = new Employee();
    durval.setName("durval");
    durval.setLastname("da Silva");
    durval.setCpf("123.123.123-X");

O motivo é simples: o construtor é o contrato que nos diz: “para você obter um novo objeto tipo Employee, você precisa obrigatoriamente me informar: Nome, Sobrenome e CPF, pois caso contrário, você criará um Employee inválido.”

E é justamente o que o exemplo incorreto faz: cria um Employee sem dado algum. Quem garante que eu irei chamar setName, setLastname e setCpf depois? Eu posso esquecer. Pode nem ser eu o responsável por utilizá-lo depois. (APIs-like).

Lembre-se o construtor é a certidão de nascimento do objeto. Não tire dele esse direito. Brasil, país rico é país…

Cresce

Aqui temos outros métodos que mudarão o status de nosso objeto ao longo de sua vida. Nem sempre é com setter – outro erro comum daqueles que programam por coincidência. Veja exemplos:

Correto:

    employee.exame_admissional = arquivo_pdf_do_exame  # snake case r0cks

    employee.setExameAdmissional(arquivoPdfDoExame)

    employee.definir_credencial_de_rede(username: "new_username", password: "983hJfh78") 

Incorreto:

    objeto_credencial = Credencial.new(username: "new_username", password: "9789237498")
    employee.credencial = objeto_credencial

    objetoCredencial = new Credencial("new_username", "3798472398478234")
    employee.setCredencial(objetoCredencial)

Neste caso, o incorreto viola o encapsulamento da Credencial, deixando a cargo do cliente conhecer como a Credencial é criada. Para entender melhor, desenho:

Quebra de Encapsulamento

Cliente é toda classe que utiliza outra. Só para deixarmos as coisas claras por aqui.

O problema aí é que no caso o Controller precisa saber como construir Employee e Credencial, além de precisar conhecer como relacioná-las. Um exemplo mais OOP seria:

Aggregate Root

Agora, o Controller precisa apenas lidar com Employee e este lidará com a Credencial. Em pseudocódigo, ficaria:

    # Ruby
    class Employee
      # construtor e outros métodos ocultados

      def define_credencial(username: username, password: password)
        @credencial = Credencial.new(username: username, password: password, employee: self)
      end
    end

    # Java, et al.
    class Employee {
        private Credencial credencial;

        public void defineCredencial(String username, String password) {
            credencial = new Credencial(username, password, this);
        }
    }

Neste caso, Credencial é um Value Object pertencente ao Employee. Esse tipo de relacionamento é parte do Domain-Driven Design e é chamado de Aggregate Root.

Aggregate root são úteis quando você tem um relacionamento aonde a Parte (Credencial) é totalmente dependente do Todo (Employee). Em outras palavras: a Parte só tem vida quando o Todo tem vida. Não é legal que uma Parte tenha vários Todo(s).

Continuando disponível

Em orientação a objetos, nossa linha de pensamento deveria ser um pouco mais ampla. Ou seja, devemos pensar que uma vez que o objeto é criado (instanciado) ele ficará disponível até que alguém o mate (remova da memória). Enquanto ninguém o remover da memória, ele continuará vivo.

É importante esquecer o meio/tipo de armazenamento aqui. Robert Martin (@unclebob) falou sobre isso na sua must watch palestra na Ruby Midwest 2011 – sério: assista até o fim. Você será outro depois disso 😉

Morre

O objeto é removido através do Garbage Collector, Removido com ORM, etc. Aqui não há novidade mesmo.

1 imagem, 1000 palavras

Object Life Cycle

O meio de vida do objeto (aquele dentro do retângulo) é aonde a maior parte dos objetos estarão. Ao buscar um objeto com um Repository por exemplo, devemos pensar que o objeto estava em memória, já construído e do jeito que o deixamos da última vez que trabalhamos com ele. Ou seja: o Repository não dará new novamente, pois o objeto já existe. Pelo que me lembro, o Doctrine 2 seguia isto: ao recuperar um objeto do Repository o construtor não era chamado novamente – o que é excelente, pois o Ciclo de Vida do Objeto não é quebrado!

Concluindo?

O tema é extenso assim como Test-First. Conhecer bem o que a Orientação a Objetos reserva e espera de você é pré-requisito para iniciar com Teste de Software em aplicação Orientada a Objeto. Na sequência, continuarei a falar sobre Object Oriented Design para permitir que se veja além ao testar software.

Obtendo o primeiro Test Pass

Depois de ficar maravilhado com as promessas de um mundo melhor código mais harmonioso, manutenível e desacoplado, vem uma curta pergunta que nos leva a nossa primeira Rua sem Saída: como fazer meu primeiro teste (do projeto) passar?

As minhas Ruas sem saída foram: “em um projeto novo, o que testar primeiro?” e, “em um projeto legado como fazer o primeiro teste?”

Vou criar um cenário hipotético, apenas para fins de apoio: você é contratado para fazer uma Extranet de Bike Shop. Os requisitos em alto nível são:

  • Área segura para somente funcionários/gestores logarem no sistema

Somente após login, a aplicação liberará acesso às features:

  • Cadastro de Marcas e Modelos de bicicleta;
  • Cadastro de bicicleta;
  • Cadastro de peças de reposição para uma (ou várias) marcas de bicicletas. – Pense nessas peças como peças de um determinado fabricante de carro: você pode comprar a peça “original” com a logo do fabricante do carro na peça ou a “paralela” feita pelo mesma empresa (por ex. Cofap, de amortecedores) mas sem a logomarca de nenhuma fabricante de carro, em um CarShop qualquer de rua.
  • Cadastro de peças genéricas (a tal da paralela).
  • Listagem de tudo isso que foi pedido.

Para efeitos de exemplos:

Marca: Caloi
Modelo: Urbe
Peça de reposição: Guidão Flat
Peça genérica: Cambio Traseiro Shimano TX 7 Velocidades

A peça genérica em nosso bike shop, em tese servirá para qualquer bicicleta que precisar daquele componente.

Começando: você escolhe a linguagem de programação, um possível bootstrap para subir um Hello Test no navegador e pronto. Está pronto para começar a fazer as coisas. Mas, e agora?

Sabendo por onde começar

A pergunta inicial não deveria ser “Por onde começar?” mas sim, “O que é mais importante no projeto?”. Seu cliente/chefe irá ver o projeto de tempos em tempos e você certamente terá dúvidas que só ele saberá responder. Enquanto o projeto não está pronto, o que é dispensável no projeto?

O sistema de autenticação é claro! Incrivelmente com todos que falei ou fiz essa pergunta (incluindo eu mesmo – sim, falo sozinho), o sistema de autenticação é escolhido como primeira coisa a ser feita. Pense bem. Para que raios a autenticação é importante agora? Ela evitará acesso indevido, mas se o software está em processo embrionário, com acesso limitado ao servidor e dados dummies, para que ele serve? Ignore-o por enquanto e foque no core do negócio!

Os pingos nos ís

Nat Price, sugeriu em seu livro criar um Teste de Aceitação e com este, criar seu primeiro Teste Unitário de Unidade. Seguiria criando os testes de unidade fazendo-os passar até que o teste de aceitação passaria também, finalizando assim aquela feature.

Ciclo do TDD por Nat Price

Eu já fiz essa abordagem, mas achei ela um tanto verbosa em casos onde o do teste de aceitação é muito parecido com o teste de unidade – até porque um software web é arquitetonicamente diferente de um software de missão crítica ou embarcado, por exemplo.

Atualmente, eu sigo apenas o processo interno, sem Teste de Aceitação ou/e Integração pelo simples fato de ser mais direto e não há nenhum efeito colateral no processo em si.

TDD Red Green Refactor ciclo

O Primeiro Green, Like a Boss!

Eu começaria pela Peça. Uma classe para identificar Peças “Originais” e outra para Peças Genéricas ou apenas uma?

O legal é esse tipo de decisão, pode ser adiado com Test-Driven Development. Vamos começar pelo mais fácil: uma classe Peça.

    @test
    everyGenuinePartBelongsToABicycle()

Assim como em Concessionárias de automóveis, há peças que são relacionadas ao veículo e por mais que sirva em mais de um modelo, a relação Parte-Veículo torna-se indispensável.

Algo assim, pode ser um começo:

    class BicyclePartTest {
            private subject = new Part();
            private bicycle = a_bicycle_mock_object;

            @test
            public everyGenuinePartBelongsToABicycle() {
                subject.setBicycle(bicycle)
                assertTrue(subject.isGenuine())
            }
    }

Ou seja, para Part (Peça da bike) ser Genuína (Original), a peça precisa estar relacionada a uma bike em específico. Esse simples teste já nos guia para nosso próximo:

    class BicycleTest {
            private subject = new Bicycle();

            @test
            public willContainGenuineParts() {
                subject.addGenuinePart(a_mocked_part)
                assertCount(1, subject.getGenuineParts())
            }
    }

Uma bicicleta deverá ter Peças. Neste caso, Peça Genuína devemos atentar que a relação é uma Composição, ou seja, a Parte(Part) não existe sem o Todo (Bicycle). Já sabemos também que:

    class Bicycle {

            public void addGenuinePart(Part genuinePart) {
                parts.add(genuinePart)
                genuinePart.setBicycle(this)
            }
    }

A relação bi-direcional deverá existir, conforme o teste do BicyclePart sugeriu lá no começo.

Montando a classe Part para que satisfaça o teste do BicyclePart, você já verá o Green na tua tela. Mais do que um, dois pontos verdes.

Eu já consigo ter uma próxima dúvida: em peças genuínas, poderá haver mais de uma peça por bike? Sim, não?

Sim! Pode-se optar por quantificar quantas peças daquela a bike tem e aonde elas ficam:

    class BicyclePartTest {
            private subject = new Part();
            private bicycle = a_bicycle_mock_object;

            @test
            public genuinePartMustHaveQuantityAndPosition() {
                subject.setBicycle(bicycle);
                subject.definePositionAndQuantity("front", 2)
                subject.definePositionAndQuantity("rear", 2)

                assertEquals(4, subject.getRequiredQuantity())
                assertEquals(["front", "rear"], subject.getPositions())
        }

            @test
            public everyGenuinePartBelongsToABicycle() { ... }
    }

Implementando o código de produção, o teste passará mais uma vez. 3 greens!

E Part não Genuína? Também terá posicionamento e quantidade? Uma porca do Cubo da roda, por exemplo são duas na frente (direita e esquerda) e duas atrás (direita e esquerda). Então:

    class BicyclePartTest {
            private subject = new Part();
            private bicycle = a_bicycle_mock_object;

        @test
            public partMustHaveQuantityAndPosition() {
                subject.definePositionAndQuantity("front", 2)
                subject.definePositionAndQuantity("rear", 2)

                assertEquals(4, subject.getRequiredQuantity())
                assertEquals(["front", "rear"], subject.getPositions())
        }

            @test
            public everyGenuinePartBelongsToABicycle() { ... }
    }

Neste caso nem precisei fazer outro teste. Apenas mudei o input e nome do teste anterior. Agora o teste diz que uma Part precisa ter quantidade e posição. Ok, mas e se eu não a definir? Uma vez que é obrigatório (..) uma ideia é mover para o construtor de Part, não? A simples ideia de fazer por steps, nos faz pensar sobre o negócio que estamos criando e consequentemente, criar um código mais legível e coeso (harmonioso, por exemplo).

Mas, mas…

E o Database Schema? A herança do ActiveRecord ou o Repository do DataMapper?
Uma coisa que eu faço – até com Rails – é criar POJO’s/PORO’s/POPO’s – Plain Old (Java | Ruby | PHP) Objects. depois que eu tenho uma relação mínima feita, daí sim eu crio as relações, os schemas, etc – pois eu sei que precisarei isolá-los e etc.

A autenticação ficou para uma fase mais madura do projeto. Poderá ser até mais para o fim, quando realmente ela for útil.

Concluindo

Seguir o de sempre e começar sempre pelo mesmo lugar não é algo justo de ser feito. Talvez você precise de uma Introspecção para rever algumas práticas (..)

Criar software orientado a testes é muito divertido, você precisa tentar, sério!

Challenge

Se você quer tentar, pegue esse simples projeto que citei aqui e tente implementá-lo em com sua linguagem do momento. Se quiser, pode me enviar por Github para te dar umas dicas. Se quiser também, podemos marcar uma conversa para discutirmos o design da aplicação. Será muito valioso para todos participantes. 😉

As ruas sem saída em Test-first

Uma vez entendidas as motivações de se fazer Test-first, mudar seu mindset para TDD, muitas coisas ainda restam apontar. Claramente, há um processo sugerido para conseguir se concentrar e tomar como guia quando chegar em becos sem saída trabalhando com Test-first.

Tão sabido quanto ao andar de bicicleta você fatalmente cairá uma hora ou outra, com os testes não é diferente: você irá sim chegar à ruas sem saída. Terá dúvidas, poderá sentir-se no escuro. Não pelo Test Driven Development per se, mas pelo fato de que você está saindo da sua zona de conforto; você está experimentando.

Na minha máquina (não) funciona!

pessoas que de tanto chegar à ruas sem saída, acabam assumindo que TDD e toda aquela coisa de design desacoplado são cartas viradas. É fato de que a ciência que envolve a computação é muito recente. Vivemos nosso momento paleolítico e haverão grandes cientístas (e tecnologistas) no futuro mudando a forma como lidamos com tais problemas, mas não podemos simplesmente desistir e dizer: isto não funciona. A ciência não funciona assim. Para poder dizer esta por**** não funciona você precisa dominá-la bem. Precisa apresentar todo um estudo defendendo seu ponto de vista baseado em dados, experimentos e outros estudos. Há artigos e não posts em blog provando a eficiência do Test Driven Development para projetos de software em nosso momento da história da computação. Obviamente se você pretende fazer apenas CRUD utilizando um framework One Size Fits All – que não conterá regra de negócio alguma, o Test-first torna-se desnecessário.

Rua sem saída

Rua sem saída – a.k.a ficar sem ideias ou não saber como prosseguir – não é algo ruim. Quando atingida, nos faz pensar sobre nossas decisões de design e como elas podem melhorar. Geralmente, ruas sem saída vêm acompanhadas de pesquisas, que puxam leituras que podem gerar debates de ideias que resultam em evolução profissional e better code.

Imagine quando começou a pedalar: você não tinha qualquer equilíbrio. Talvez não tenha utilizado rodas de apoio, mas ainda assim, não tinha equilíbrio sob duas rodas. Após várias idas à praça e várias voltas com sua bike, você começou a ganhar equilíbrio. Equilíbrio traz confiança que traz mais experiência, que te faz buscar andar mais longe e consecutivamente, mais quedas. Após crescer e ao começar a pedalar mais longe, você buscará técnicas de respiração para ir mais longe com menos esforço físico. Se guia sua moto, irá pesquisar técnicas de pilotagem e direção defensiva. Com Test-first o processo é exatamente o mesmo, adaptado para software: inicia em Test-first, busca por técnicas, experimenta; falha; pesquisa mais; tenta novamente; obtém o resultado, aprendendo a técnica; inicia o ciclo novamente.

Test-first é um conceito. Assim como design de software. Isto quer dizer que não existe uma resposta certa; mas sim, soluções aceitáveis. Para obter uma das soluções, você precisa:

  1. Ler livros, papers;
  2. Discutir com co-workers, listas de discussão;
  3. Praticar;
  4. Praticar;
  5. Praticar.

Equipando-se para se proteger em becos escuros

Como dito anteriormente, há algumas known tips que ajudarão você a manter-se focado na solução. Unclebob, já falou inúmeras vezes sobre isto em seu(s) blog(s), vídeos e palestras. Um conhecido é o Three Rules of TDD.

  1. Você deve sempre começar a feature, criando um teste daquilo que deseja obter como resultado – e o teste deve falhar.

  2. Você deve escrever apenas um teste de unidade por vez e este precisa falhar. Lembrando que não compilar é um erro também.

  3. Você precisa escrever apenas o necessário para aquele código passar. Lembre-se de que TDD é para preguiçosos.

Pode parecer um tanto dogmático, mas depois de entender os objetivos do Test-first, essa coisa faz sentido.

Testes são especificações. Um conjunto de especificações formam um software funcional. Working software é o objetivo que devemos ter. De nada adianta um monte de prática se ao final o software não é entregue como deveria. Sempre tenha isso em mente: esse teste precisa ter um propósito claro dentro do meu projeto. Se o teste não é claro, pode ser que você esteja caminhando para um beco escuro.

Red, Green, Refactor. É uma versão amigável das três regras do TDD. Teste falhando; Teste passando; Limpar seu código de produção para deixá-lo o mais legível e simples possível.

Simples? Métricas para Simples:

Single Responsibility Principle. Seu método, sua classe, seu módulo(mixin, trait, etc) precisa fazer apenas aquilo que se propôs a fazer. Métodos do objeto devem ser claros, objetivos.

Outra métrica é o DRY. Don’t Repeat Yourself. Fez copy-paste parcial ou totalmente de um trecho de código para utilizar em outro lugar? Duplicou o código.

Law of Demeter ou Tell do not ask: apesar do nome medonho, a coisa é fundamental estar registrada na tua cabeça: ao invés de perguntar algo de um objeto para com o retorno do método fazer alguma coisa, peça que esse objeto faça o que você quer utilizando seu input. Exemplo:

    public class Order {

      public void add(Produto itemComprado, int quantidade) {
        if (itemComprado.estaAVenda() && itemComprado.getEstoque().getQuantidade() >= quantidade) {
          OrderItem item = new OrderItem(itemComprado, quantidade, this);
        }
      }
    }

Ao invés de expor as particularidades do Produto e Estoque para o Order, podemos simplesmente fazer justiça com as próprias mãos:

    public class Order {

      public void add(Produto itemComprado, int quantidade) {
        if (itemComprado.temDisponivel(quantidade)) {
          OrderItem item = new OrderItem(itemComprado, quantidade, this)
        }
      }
    }


    public class Produto {

      public boolean temDisponivel(int quantidade) {
        return this.estaAVenda() && this.estoque.temDisponivel(quantidade);
      }
    }

Assim, Order#add agora passa apenas a adicionar produto à Order, sabendo apenas que precisa saber se tem estoque para tal. Como saber se tem estoque é problema do Produto e seu estoque não de Order.

Apesar de parecer óbvio, é um dos code smells que mais fiz e vi durante esse tempo. É natural falando/escrevendo, mas no meio do seu código, isso passa batido facilmente. Keep on track.

Concluindo

Técnicas e conceitos como S.O.L.I.D, precisam estar em nossas cabeças para evitarmos andar em direção aos becos escuros. Ruas sem saída, são parte do processo de aprendizado que são mitigadas com leitura e muita prática. Test-first não se aprende em um mês ou dois. Nem por isto, testing é uma coisa chata. Em minha opinião, os testes tornam o dia-a-dia muito mais divertido, desafiador e proveitoso.

Mais do mesmo

Gostou do assunto? Entre Julho e Agosto escrevi exclusivamente sobre problemas com as Ruas sem Saída em Test-first. Veja a lista:

Introspecção

Mudança. Todos temem à mudanças. E não seria diferente ao codificar software, não é mesmo?
Para que mudar se podemos continuar com nossa atual rotina simples e praticamente automática? Aquela coisa de chegar no trabalho, fazer o de sempre, como aprendeu no tutorial há tempos atrás ou que viu o ex-chefe fazer e até hoje executa assim sem questionamentos.

Em software isso recebeu um nome no livro The Pragmatic Programmer. Caso o cenário acima encaixe no seu code-style, você está programando por coincidência. Gosto do termo em português, pois ele é duplo sentido. Além de indicar o estado profissional acima, ele também implicitamente diz que você está na área por um mero acaso. * O livro não fala disso. Essa segunda parte é totalmente de minha autoria. Pode parecer xiita, mas te convido a refletir sobre isso mais a fundo.

Ao entrar no seu modo automático e passar a fazer coisas sem se questionar o porquê está fazendo (ou fazendo novamente) aquilo, você está programando por coincidência. Seguir aquele fluxo toda vez que depara com um problema passado sem questionar-se o motivo e se há melhor alternativa, você está programando por coincidência. Se, você assume coisas – isso vale muito pra teste – por exemplo que o erro X é causado pelo Y, simplesmente porque você acha que isto ocorre, você está programando por coincidência. Neste caso você não deveria assumir, mas sim provar que o problema é aquele mesmo.

Saindo da coincidência

Uma das formas de evitar esse ciclo de tédio e comodismo é justamente fazer Test-First. Explico os motivos:

  1. Lego Effect: como comentei no Teste Unitário?, começar pelo teste fará você pensar em como aquela funcionabilidade que pretende fazer irá se relacionar. Isso te faz pensar mais e consequentemente, dúvidas virão e você buscará saná-las (livros, pesquisa na Internet, Twitter, outros devs, projetos Opensource).
  2. Ah, a preguiça! Os testes te ajudarão a achar o menor caminho possível para aquela implementação. (teste é coisa de preguiçoso, lembra?) Isto te fará repensar técnicas. Com a prática de novas técnicas, você evoluirá como desenvolvedor.
  3. Não mais dúvidas sem resposta! Com teste, você pode testar não apenas o código de implementação mas também as suas suposições. Pensamentos do tipo: será que isto pode funcionar desta forma? serão mais frequentes, uma vez que o espaço entre o será que… e a resposta são apenas escrever um assertion simples e executá-lo.
  4. You on Rails. A prática do Test-first te mantém na linha de fazer o que realmente importa. Sem suposições vazias; Sem achismo; Você se antecipa dos problemas que poderá encontrar. Estar alinhado em fazer o que importa te torna introspectivo.
  5. Test & fail. Test & Learn.
  6. Não sendo mais telespectador. Você no controle! Não mais sendo guiado por processos e rotinas sem saber o motivo de estar fazendo aquilo daquela forma.

Questione-se; Reflita; Pense um pouco mais sobre sua forma de fazer as coisas. Isso é realmente a melhor forma de se obter o resultado que deseja?

Recentemente peguei meu Card Deck que ajudei a fundar (?) no Kickstater. Um dos cards que mais me chamou a atenção, até pelo fato de eu já ter feito isso anteriormente é o Remove your BEST idea.

Remove your best idea

Conseguiu achar uma solução para o problema? Ótimo! Ela poderá ser seu backup caso as coisas dê muito errado. Empenhe-se para achar uma outra melhor ideia. Vale muito a pena esse exercício.

Concluindo

Test-first é uma das formas de acabar com a programação por coincidência. Test-first entretanto não irá te salvar de todo o mal do mundo – mas posso afirmar, por experiência própria que ele te coloca on track novamente.

A propósito, já leu o The Pragmatic Programmer: From Journeyman to Master? Permita-me dizer que você deveria se ainda não o fez pelo menos uma vez.

Test Double com Mock e Stub

Lembro-me bem da primeira vez que ouvi o termo. Era meados de 2009, quando li em algum lugar do GUJ sobre um “recurso” muito valioso nos Testes Unitários que era de fundamental entendimento para seguir adiante. Depois de ler algumas coisas na Internet, eu havia encontrado pelo menos três diferentes definições para Mock e Stub. Naquele momento eu descobri de que precisava de bibliografias-referência no assunto para acabar com aquelas meias verdades que pairavam sobre minha cabeça.

A primeira definição que encontrei era mais ou menos assim: “Mock você utiliza quando o valor de retorno importar; Stub nos demais casos.” – ah, quantas vezes não “testei” software com esse mindset. Mesmo não entendendo bem o que era o tal valor de retorno, eu me aventurava e me forçava a fazer. Segui assim até ler pela primeira vez o livro Growing Object-Oriented Software, Guided by Tests, de Steve Freeman e Nat Price. Naquele momento minha cabeça explodiu e tudo fez mais sentido. Desde então, costumo defini-los da seguinte forma:

“Stub é uma dependência da sua classe-alvo (Objeto em Teste), agindo como um substituto, evitando que a implementação real seja executada.”

Explicação longa:

    class Authenticator
      def login(user)
        return user.password == "123456"
      end
    end

    describe Authenticator do
      it "will login with valid credentials" do
        user = double('User', password: '123456')
        expect(subject.login(user)).to be_true
      end
    end

Repare no teste o user = double('User', password: '123456') e repare que isto permite que eu simule um usuário “válido” (no exemplo é só o password bater com 123456) – ou seja, eu configurei minha dependência (User) para que o objeto Authenticator pudesse ter um usuário válido. Um exemplo mais elaborado seria:

    class Authenticator
      def login(user)
        if (user.admin? and user.has_confirmed_account?)
          self.grant_permissions_to(user)
        else
          false
        end
      end

      private
      def grant_permissions_to(user)
        # do something nice with our lovely user
      end
    end

    class User
      def initialize(sms_api: SMSApi.get_instance)
        @sms_api = sms_api
      end

      def admin?
        type == 'admin' # type could be an Database field mapped
      end

      def has_confirmed_account?
        user.documentation_already_approved? and sms_api.cellphone_confirmed_for(self)
      end

      private

      def sms_api
       @sms_api # SMS Wrapper Injected via initialize (constructor) method
      end
    end

    describe Authenticator do
      it "will login with valid credentials" do
        user = double('User', :admin? => true, :has_confirmed_account? => true)
        expect(subject.login(user)).to be_true
      end
    end

Deixei o exemplo mais elaborado para mostrar o poder e a importância do Test Double: note que stubando o método User#has_confirmed_account? eu simplesmente evito ter que lidar com o SMSApi e com o documentation_already_approved?, bastando eu ter feito meu double retornar true no has_confirmed_account?. Imagina o trabalho que eu teria para configurar o SMSApi e o método de documentação aprovada em todo teste que eu precisasse chamar o método has_confirmed_account?. Insano, né?

Graças ao double eu consigo focar no meu problema que é: o Authenticator#login está, dado um usuário aceito por ele, conseguindo autenticar este user object?

Repare que o método grant_permissions_to(user) não é stubado. Ele precisa ser chamado de verdade pois é um colaborador interno da classe-algo (Authenticator class).

Mock é ligeiramente diferente, precisa estar atento para entender as diferenças.

“O Mock irá criar a expectativa de que aquilo que você definiu irá de fato acontecer. Se não acontecer, o teste falhará.”

    describe Authenticator do
      it "will login with valid credentials" do
        user = double
        expect(user).to receive(:admin?).once.and_return(true)
        expect(user).to receive(:has_confirmed_account?).once.and_return(true)
        expect(subject.login(user)).to be_true
      end
    end

Assumindo o exemplo anterior, modifiquei apenas o teste, trocando Stub por Mock. Trocando quando e se, encontrar user.admin? e/ou user.has_confirmed_account?, substitua por true e true respectivamente para você (Authenticator#login) deverá chamar user.admin? e user.has_confirmed_account? (em qualquer ordem no método) apenas uma vez (once), e terá true e true respectivamente como resposta. Saímos de algo simples, para algo assertivo. Se por um acaso eu trocar o código de produção para:

    class Authenticator
      def login(user)
        if (user.has_confirmed_account?)
          self.grant_permissions_to(user)
        else
          false
        end
      end
    end

O teste neste caso começará a falhar, reclamando a falta do user.admin?.
Naturalmente, há regras e boas práticas para quando testar expectativas via Mock’s e quando não. Sandi Metz abordou o tema neste Lunch ‘n Learn.

Mock/Stub parciais (Partial Mocks)

Um recurso suportado pelo RSpec são os Partial Mocks. Há quem defenda o não uso deles (Prophecy @ PHPSpec, estou olhando para você!). Mock parcial, permite que você utilize um objeto real como dependência e mock apenas determinados métodos dela. Ainda continuando com o exemplo do autenticador, teríamos:

    describe Authenticator do
      let(:admin_user) { User.new(...) # faz alguma coisa para construir um Usuário admin? == true}

      it "will login with valid credentials" do
        expect(admin_user).to receive(:has_confirmed_account?).once.and_return(true)
        expect(subject.login(admin_user)).to be_true
      end
    end

As diferenças aqui são:

  1. Não utilizamos um double do RSpec. Preferimos utilizar o User object de verdade, fazendo o que for necessário para criar/retornar um usuário administrador, ou seja, um objeto de User cujo o método #admin? retornará true sem a necessidade de mudar o valor de retorno do método com stub.
  2. Com isto, não precisamos definir no teste it... o retorno de #admin? (pude remover a linha)

Com isto, ainda assim, eu mockei o #has_confirmed_account? para continuar retornando true. Com isto, acabei fazendo um Mock Parcial: o método #admin? é chamado de verdade e o #has_confirmed_account? é mockado para retornar sempre true naquele teste.

Concluindo

Mocks e Stubs são fundamentais para a construção do seu design e para seguir em frente com Test-Driven Development. Neste post, busquei mostrar, com definições mais simples, o que são ambos e dar foco nas suas diferenças. Mas não pense que sair mokando/stubando tudo é boa prática. Há situações onde você não deve mockar; há situações onde o mock aponta um possível problema de design – e então você precisa refatorar seu código e talvez criar uma nova layer na aplicação. Em todo caso, pratique muito o assunto e torne seu código mais legível, testável e plugável.

Happy Mocking 😉