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 😉

Teste Unitário?

ad: Há muito mais sobre a temática Test-First aqui no blog. Dê uma olhada nos posts mais recentes e assine o feed dos posts 😉

Ponto crucial para um possível futuro level up como Test-First e Test-Driven Development Design, o Teste Unitário de Unidade, em minha opinião, aparece como ponte entre fazer testes vs se frustar com eles.

O Teste Unitário de Unidade consta como principal forma de teste em várias literaturas que abordam o tema. Entender como criar testes de unidade para seu software é decisivo para adoção da divertida (e emprego keeper) forma de codificar software.

Primeiramente, é bom enfatizar que teste de unidade não é testar todo método de seu objeto, mas sim testar toda sua interface pública que contém regras de domínio. Vamos tomar o seguinte exemplo:

class User
  attr_reader :name, :birthdate

  def initialize(args)
    # do something to build object
  end

  def sent_packages
  end
end

No exemplo, temos o construtor (initialize), o método sent_packages além dos getters name e birthdate.

Para este caso, os getters retornarão o dado exatamente como ele foi inputado – prefiro o termo injetado – ou seja, os getters não contêm nenhum acréscimo para o negócio (software), por isto, não é necessário criar testes para eles, pois o Ruby tem o teste que garante o correto retorno para o dos valores em memória no attr_reader. Se o attr_reader já foi testado pela linguagem e o seu getter não tem nenhum acréscimo (formatação, contatenação, etc), não precisa-se criar teste para isto.

Um teste válido neste caso seria construir um usuário válido e verificar o retorno do sent_packages.

Vamos deixar a classe mais interessante:

class User
  attr_reader :name, :birthdate

  def initialize(args)
    # do something to build object
  end

  def sent_packages
    do_some_weird_stuff
    # do more things
  end

  private

  def do_some_weird_stuff
  end
end

Ainda assim, deve-se continuar com os testes anteriores, pois não é recomendado testar métodos privados/protegidos isoladamente. Devemos sim, testar a interface pública – o método público – que chama o método privado que no exemplo acima é o sent_packages.

Aqui vale um parênteses importante sobre métodos privados: Sandi Metz afirma em seu livro que devemos levar todo método privado como um contrato que nos diz claramente: você pode até usar meu método privado, mas lembre-se: eu não garanto que ele continuará retornando o que retorna hoje numa próxima versão. Ele poderá até ser removido. Essa afirmação é válida, pois em Ruby você pode acessar qualquer método de um objeto mesmo os privados. Isso porque em Ruby, que segue o que eu já ouvi falar por “Smalltalk OOP”, a orientação a objetos é pura e simplesmente troca de mensagens. Depois que adotei este mindset, a troca de mensagem e colaboração em minhas classes aumentaram significantemente.

Teste Unitário de Unidade não deixa haver colaboração entre outros objetos além do alvo do teste. O que isso significa em termos práticos?

class User
  attr_reader :name, :birthdate

  def initialize(args)
    # do something to build object
    @packages = []
  end

  def sent_packages
    packages.map { |package| package.sent? }
  end

  def packages
    # get user packages from somewhere (ORM, Repository, etc)
  end
end

class Package
  attr_reader :track_number, :post_date

  def sent?
    !!track_number
  end
end

O sent_packages acima pode te lembrar várias coisas: Rails ActiveRecord; Repository acessando dados de um DataMapper ORM ou uma simples iteração objeto-coleção.

Repare que a classe User precisa falar com a classe Package para verificar quais dos packages foram sent. Não podemos deixar que o teste da classe User permita que o objeto consiga falar com Package. Ou seja:

describe User do
  subject { User.new } # only to show how RSpec works to non-Ruby developers

  context "when User relates with Package" do
    let(:all_packages) do
      [
        double('Package', :sent? => true),
        double('Package', :sent? => true),
        double('Package', :sent? => false)
      ]
    end

    let(:sent_packages) { all_packages[0..1] }

    it ".sent_packages" do
      allow(subject).to receive(:packages).and_return(all_packages) # this is a stub!
      expect(subject.sent_packages).to be_equal sent_packages
    end
  end
end

Mesmo que você não entenda Ruby nem as DSL’s do RSpec, repare que no let(:all_packages) eu utilizo um recurso chamado double que nada mais é do que um objeto simples que pode ter qualquer nome. No caso, o chamei de ‘Package’. O sent?: boolean diz ao double que quando o método sent? dele for chamado, deverá retornar true dos dois primeiros e false no último. Isso é equivalente a:

class Double
  def sent?
    true # and false in the last one.
  end
end

Já o let(:sent_packages) retornará uma parte da Collection all_packages, no caso os dois primeiros doubles que retornam true no método sent?.

Finalmente, o it que é o teste, faz duas coisas: na primeira linha ele diz que User#packages deverá retornar a collection all_packages. Aqui aconteceu a mágica: com isso eu não deixo o User falar com o Package de verdade. Eu faço o User retornar uma collection que eu tenho total controle. Já explico o motivo disso.

Na segunda linha, eu crio uma expectativa que é: o método User#sent_packages deverá retornar apenas os Package que tenham sent? igual a true.

O motivo de utilizar o Stub (ali no allow...) é fazer que o método packages de User sempre retorne o que eu quero de uma forma controlada. Como o método package nada mais é do que um delegator para um Repositório ou mesmo um has_many :packages do Active Record, isto é, uma dependência externa (outra classe), eu posso stubar ou mockar.

Dica: não mock/stub método de sua própria classe/objeto a menos que seja um delegator puro para outra classe, como no exemplo.

Dica polêmica: Only Mock what you own. Mock apenas o que você domina. Ou seja, classes criadas para o software que você está trabalhando.

O que você testou afinal?

Neste teste eu quero apenas saber se o User#sent_packages sabe filtrar de todos os meus packages, apenas aqueles que foram sent e ele sabe!

def sent_packages
  packages.map { |package| package.sent? }
end

Isso porque sempre devemos presumir que a classe relacionada já está testada individualmente. Em outras palavras: devemos assumir que Package já está testada isoladamente (Teste de Unidade).

Porque não deixar User falar com Package?

Por quê isso que é Teste de Unidade. Se eu deixasse User falar com Package, deixaria de ser teste de uma unidade do sistema e passaria a ser duas – e não queremos isto, né? Pois isto criaria um acoplamento entre User e Package e não me evitaria a deixá-las isoladamente funcionando. Quando você não se atenta para esse tipo de coisa, você acaba deixando seu teste acessar o banco de dados ou aquele serviço REST ou SOAP. Isolamento como o acima, deve acontecer toda vez que o objeto que você está testando precisar falar com outra classe(objeto dela).

Concluindo

Teste de Unidade é um assunto longo, envolve muito mais do que simplesmente saber os do and don’t, pois Teste de Unidade é questão de design.

Às vezes, você ficará tentado a testar mais de uma unidade, deixar o teste acessar o database, a API e outras classes do seu domínio. A prática leva a perfeição, dizem.

Final Alternativo (e melhorado)

class User
  attr_reader :name, :birthdate, :repository

  def initialize(args, repository: DomainRepository.new)
    @repository = repository # hey, I am an injected dependency class
    @packages = []
  end

  def sent_packages
    packages.map { |package| package.sent? }
  end

  def packages
    repository.packages_for(self)
  end
end

class Package
  attr_reader :track_number, :post_date

  def sent?
    !!track_number
  end
end

Sem o ActiveRecord do Rails, o exemplo acima seria uma possibilidade: injeção do Repository via construtor ou setter. Desta forma, o mock não ficaria no método packages do User, mas sim, no Repository injetado o que é epic win. Veja:

describe User do
  context "when User relates with Package" do
    let(:all_packages) do
      [
        double('Package', :sent? => true),
        double('Package', :sent? => true),
        double('Package', :sent? => false)
      ]
    end

    let(:repository) { double('Repository', packages_for: all_packages) }
    subject { User.new({}, repository: repository) } # Stub Repository injected

    let(:sent_packages) { all_packages[0..1] }

    it ".sent_packages" do
      expect(subject.sent_packages).to be_equal sent_packages
    end
  end
end

Com a adição de let(:repository) que é injetado no subject {..}, foi possível remover aquele stub allow(subject).to ... do teste, deixando-o mais limpo, legível e plugável. Substituí uma injeção direta pelo stub da classe Repository. Este é o ideal, mas nem sempre é possível evitar que um Rails apareça com seu Active Record e deixe as coisas um pouco mais… complicadas. Não caia no engano de achar que apenas o Rails é vilão: até hoje não achei um framework ORM de Active Record que fosse plugável como um DataMapper é.

É isso sempre que buscamos com o Teste de Unidade: desacoplamento. Retirar coisas e torná-las injetáveis em nossas classes com o objetivo de passar e trocar mensagens entre os métodos de nossos objetos igualmente desacoplados. Isso é a base para criar seu design orientado a objetos.

O que Test-Driven Development não é

Uma dúvida muito pertinente em engenharia de software é sobre Test-Driven Development, TDD para os mais chegados. Há resistência por parte daqueles que não conhecem – ora, também não é para menos: o nome remete a Teste e teste remete a um processo da área de Verificação e Validação.

Após ouvir o developer dizer nunca fez e nunca viu “fazerem TDD” e suas argumentações sobre o porque isso não funciona, costumo perguntar uma simples pergunta: para você, o que é TDD ?

Até o momento as respostas foram uma derivação de: “Serve ahn… pra você verificar se seu software funciona (..) serve para evitar bugs”. Ora, não é para menos que você desacredita – e por isso nunca tentou – na ideia.

Redução de bugs é um efeito do TDD e não seu objetivo. Alguns já não definem mais a sigla como Test-Driven Development, mas sim como: Test-Driven Design o que na minha opinião é muito mais descritivo para os “novatos no assunto”. Este novo nome deixa tudo mais claro: TDD é sobre a construção do seu design. Pronto. Só isso.

Partindo do princípio que você saiba o que “design” significa em engenharia de software, aposto que está repensando em tudo que você argumentava (se é que) contra a adoção do estilo de desenvolvimento. Explicado o que é o Test-Driven Development Design há ainda outras barreiras a ser vencidas. Vamos a elas.

1. TDD consome mais tempo

Depende. No início é óbvio que você irá mais devagar, pois estará aprendendo algumas coisas enquanto tenta implementar uma feature:

  • A técnica de codar seguindo TDD;
  • A técnica de codar Testes Unitários de Unidade;
  • O framework de teste;

Codificar seguindo o Test-Driven Design (vou repetir isso inúmeras vezes para fixar), após estudar e pegar prática, você irá produzir mais do que antes. Fato. Além disso, provavelmente o deixará pensando bastante sobre pontos como:
como devo começar meu teste? O que devo testar? Como testar? e, até onde testar?

Note que “testar” neste contexto significa: verificar que o método está fazendo o que se espera que seja feito; Apesar de parecer óbvio, é melhor ter isso sempre em mente.

Teste de Unidade é um ponto importantíssimo para conseguir seguir em frente com o Test-Driven Design. Isso dá um post dedicado, por isso podemos discutir sobre o que não é Teste de Unidade.

Ele não “testa” necessariamente todos os métodos da sua classe. Um erro comum é achar que Teste de Unidade significa testar todo getter e setter de uma classe. Errado.
Quando estiver testando um método de uma classe e este método chamar outro método de outra classe, você não pode deixar essa chamada acontecer “de verdade”. Aqui entra o assunto Test Double – double, mock e stub. Também digno de outro post. Quando eu digo outro método de outra classe, estou literalmente dizendo que:

class Foo
  def do_something(another_class: MyClassDependency.new)
    # …
    another_class.find_bar(…)
  end
end

o MyClassDependency#find_bar precisa ser “mockado” ou “stubado”, pois devemos sempre partir do princípio de que ele já foi testado isoladamente e o find_bar irá retornar o objeto/valor corretamente. Por isso o Mock/Stub. (vide imagem do post ;])

Não devemos testar métodos privados diretamente. Dizem que há exceções, realmente podem haver, pois design é questão de experimentar e evoluir, porém eu em particular nunca testei método privado – mesmo em algumas situações eu ter insistido e quase feito.

O motivo é muito simples: devemos encarar nossos métodos privados como versões instáveis da nossa API. Considere toda classe do seu projeto, como uma API: métodos públicos são o canal de comunicação com a classe que tem suas interfaces (nome do método, parâmetros e retorno) estáveis. Já os métodos privados da classe, são nossos métodos instáveis, ou seja, estamos informando ao cliente (outra classe do projeto) que aquele método poderá mudar sua interface de uma versão para a outra sem aviso prévio. Isto não quer dizer que seus métodos privados não serão testados. Muito pelo contrário. Você precisa testar o método público da sua classe que utiliza o método privado em questão. Com isso, você garante não só que seu método instável (privado/protected) está funcionando, mas também que ele está bem relacionado com o(s) método(s) público(s) que o utiliza. E é perfeitamente normal sua classe possuir alguns métodos privados. Não tenha medo de utilizá-los, sempre pensando que um método deve ter apenas uma responsabilidade – um motivo para mudar.

Novamente: Teste de Unidade não deve chamar métodos de classes colaboradoras (igual exemplo acima). Ou seja: não devemos chamar nossa API/abstração de banco de dados, nem aquele Webservice de Pagamento/Notificações, nem o seu Mailer, nem seu Filesystem, etc. Você sempre deverá Mockar ou Stubar essas “dependências” da sua classe.

2. Eu gosto de “Testar” depois.

Há quem diga que já tem em mente o que precisa fazer para aquela feature funcionar e que por isto, prefere fazer o código de produção [1] primeiro e o teste depois.

Essa é fácil: se for para fazer o teste depois, você não estará construindo o design da sua aplicação. Estará perdendo tempo.

Testar depois tem dois problemas: a) perderá a chance de desacoplar suas classes, pois acredite: TDD faz você pensar mais sobre como fazer e isso incrivelmente nos faz mais produtivos, pois você resolverá os problemas de forma mais simples. b) você fará o teste assumindo que sua classe/método está correto e funciona, com isso criará o que chamam de Teste Viciado que em outras palavras quer dizer: você faz seu teste satisfazer seu código de produção, porém seu código de produção não garante que o teste está correto.

Dica simples: não deixe para testar depois. Crie o costume de fazer o teste antes!

[1]: Código de produção é o código que você deploya.

3. Minha equipe/co-workers não tem interesse pelo assunto.

Você não precisa esperar seu chefe instaurar a obrigatoriedade do Test-Driven Design. Não espere que ele saiba das vantagens disso. Aproveite o momento para você começar a espalhar a ideia na equipe.

Recentemente entrei numa equipe que não faz testes de unidade, tão pouco TDD. No começo, fiquei observando a reação das pessoas sobre Test-Driven Design e suas queixas sobre a possibilidade de fazer. Minha meta era em um mês conseguir mostrar o que a técnica de teste poderia resolver nos projetos e o quão empolgante é codificar com Test-First. Passado este um mês, eu já tinha mostrado TDD in Action para três desenvolvedores que além de ficarem convencidos e animados com a ideia, passaram a defendê-la. Eu acredito que mudanças de cultura deste tipo, devem sempre ser bottom-up, dos developers para os gestores. Muito improvável que seu gestor não aprovará a ideia se você se propuser a explicar para que o Test-Driven Design é executado em todo o mundo.

Concluindo

Test-Driven Development Design é sobre como construir seu design de forma simples e desacoplada. Sem bullshits, sem bala de prata ou santo graal. Adotar Test-Driven Development é evoluir sua forma de fazer software de uma forma muito rápida.