Rua sem saída: não consigo criar meu próximo teste – Parte IV

Seguindo para o último post da série, vou abordar o décimo item da lista inicial. Antes de continuar, veja os primeiros posts da série:

Pro tip: Lembrando que o prelúdio contém tópicos e dicas gerais que servirão para praticamente todos os itens abordados 😉

Demorei tanto pensando numa solução que acabou desistindo e criando o código sem teste. O código de produção funciona (você o testou manualmente), porém ainda não sabe como escreverá (e se) o teste que o validará

WOW! Poderia me estender e enunciar os problemas que isto te causará – isso seria um sermão chato, então sem delongas vou citar apenas um bom motivo pelo qual isto é ruim:

Você precisará testar essa rotina manualmente TODA vez em que mudar o código de QUALQUER PARTE do software

Holy moly! Isso realmente é um bom motivo, concorda? No passado, a cada novo deploy o time precisava testar manualmente todo o site – veja bem: todo o site. A cada deploy então, ficava mais complicado, pois tinha mais coisas a testar. Os bugs não chegaram a ser recorrentes, mas sempre acontecia de uma feature dada como ok quebrar devido a um side-effect maluco.

Aconteceu uma vez d’eu ver um software em produção não Test-Driven rodando. O cenário era muito crítico. Vários bugs recorrentes, horas para achar e resolver um bug sem quebrar o outro lado, etc. Eu simplesmente peguei do zero e criei uma versão test-driven. Eu não sabia das implementações (pois não participei da implementação original), logo não fui infectado pelas más decisões (de design). Comecei o software seguindo Clean Architecture citado pelo Unclebob. Ou seja: nada de persistência; nada de web-tier; nada de nada. Literalmente fui no meu ~/coding/ e rodei um mkdir: mkdir ~/coding/new-project-api. Depois criei um esqueleto de diretórios ultra simples: mkdir ~/coding/new-project-api/tests e mkdir ~/coding/new-project-api/src/$DOMAIN_MODULE.

Disso, abri um arquivo de teste novo dentro de /tests/$DOMAIN_MODULE/ e criei minha primeira especificação com Teste de Unidade. Para os demais, fui perguntando para os outros devs que tinham trabalhado no projeto inicial, perguntando sobre as features do projeto. Fiz minha versão do software, focando totalmente na arquitetura e design. Depois que tinha várias features prontas e funcionando, adicionei um ODM (Object Document Mapper) com MongoDB e simplesmente nada mudou no meu código e aquelas regras de negócio começaram a salvar árvores no Mongo de forma natural e desacoplada. Somente no final eu pensei em adicionar um web-tier para responder as requisições do HTTP Server. AH mas e a integração com o aplicativo mobile? Eles precisam sabers dos endpoints e o que virá; AH, mas e o frontend; AH, mas e meu chefe; Ah, mas e o cliente. Cada caso é um caso diferente, mas posso afirmar que no meu caso tive que lidar com tudo isso e foi bem tranquilo (após resistências de todos os lados). Acho que o pessoal tem medo de mudança, não sei. As dicas sobre isso ficarão para posts futuros – mas se você estiver em encrenca por causa disto, get in touch!

Ok, mas como escrever o teste que validará o que eu fiz? Há pessoas que fazem teste depois. Eu não sou fã disso, me acustomei com o classical TDD como chamam – mas em todo caso se você tiver experiência com Test-first e souber o que está fazendo, não terá grandes problemas em implementar seu teste. Em qualquer outro caso:

  • Como você testa manualmente?

Ignore as interações com interface e outros datasources. Feito isso, você tem praticamente seu teste de unidade mental. Basta passar para teste de unidade não se esquecendo de que mock/stub é seu amigo!

  • Pensei no item 1. mas testar está complicado.

Talvez você tenha acoplado demais sua implementação e aí o teste está te avisando disto. Recomendo ler o Prelúdio da série. Lá tem ideias gerais para quaisquer uma das 10 situações descritas aqui.

  • Meu software é um algoritmo.

Realmente. TDD não vai te ajudar muito. Se você está criando um algoritmo para resolver um problema, TDD não te ajudará em nada. Agora, se este algoritmo está inserido dentro de um Domain Model, Test-first + Test Unit te ajudará a deixar este algoritmo isolado e respondendo apenas por si só.

  • Estava testando uma integração com Paypal, Google APIs, etc.

A depender do que estava fazendo, Test-first para conseguir conectar numa API e resgatar dados simplesmente com o objetivo de sentir a API, não faz qualquer sentido. Novamente TDD é ferramenta de design de software e não de validação/verificação.

Em resumo: fazer um teste de uma parte do software depois é basicamente engenharia reversa sem os relacionamentos entre objetos/métodos. Lembre-se que isto pode custar caro uma vez que seu design sem o teste pode ter ficado horrível e o teste (de|in)testável.

Concluindo

Por fim, esta série chega assim ao seu término. Espero que tenha ajudado alguém a sair de uma rua sem saída ou no bom português, uma encrenca com test-first. Mudança de paradigma ou modo de codificar não é trivial, mas você estando disposto, é possível obter excelentes resultados a curto prazo.

Lembre-se: a curva de aprendizado e prática com Test-first é de pelo menos seis rápidos meses. Isto me lembra quando comecei a usar o Vim para codificar: enquanto eu não larguei mão dos outros editores, não consegui aprender direito a ferramenta.

Faltou alguma Rua sem Saída que gostaria que eu comentasse? Avisa aí 😉

Dúvidas, desesperos, e bênçãos via comentários nos tópicos ou como sempre, via e-mail.

Rua sem saída: não consigo criar meu próximo teste – Parte III

ERSKINE

Continuando o assunto como criar meu próximo teste, irei abordar os três tópicos seguintes do segundo post da série. Recomendo que os leiam em ordem:Continuando o assunto como criar meu próximo teste, irei abordar os três tópicos seguintes do segundo post da série. Recomendo que os leiam em ordem:

Para continuar, os tópicos 7 e 8 contêm muitas similaridades:

  • Tenho um mock que retorna um outro objeto que precisa também de ser configurado com mock. Parece um mock-de-mock.* Vou precisar instanciar vários objetos nesta nova feature.

Para ambos os casos, o problema parece emergir do simples fato de que seu alvo de teste (a classe e behavior testados) está com uma implementação acoplada a outras partes do sistema. Mas, o que isso quer dizer realmente?
Confira se o método testado está:
* Fazendo mais de uma coisa. Aqui, você pode ser menos flexível para melhor entendimento, por exemplo:

Você criar o seguinte caso de teste: “O usuário conseguirá logar, dado Login e Senha válidos“. Daí, você tenta seguir com o seguinte código de produção:

class User
  def autentica(login)
    if login.username.empty? && login.password.empty?
      raise InvalidArgumentError
    end

    # continua com o login
    end
  end
  ## nosso login ali de cima

  class UserLogin
    def username
    end

    def password
    end
  end

Pode parecer bobagem aqui, mas o método #autentica está fazendo mais de uma coisa: ele está validando input e efetuando autenticação. No seu teste, você precisaria passar um stub de UserLogin e precisaria configurar #username e #password somente para conseguir passar da parte de validação.
Este exemplo é minimalista justamente para evidenciar que casos mais complexos do mesmo problema fará com que você tenha que fazer mock de mock ou ficar instanciando/configurando um monte de objeto somente para fazer uma simples regra de negócio funcionar como deveria. Qual a solução? Vejamos:
Extrair a verificação de input do #autentica seria uma ótima. Quem sabe delegar a responsabilidade da ação para o objeto que o mereça, resolva o problema, não é mesmo? Lembra do Tell don’t ask ? Veja-o em prática:

  class User
    def autentica(login)
      if login.valid?
      end
    end
  end

  class UserLogin
    def username
    end

    def password
    end

    def valid?
      !username.empty? && !password.empty?
    end
  end

Tudo que precisamos fazer agora é configurar no seu stub que o método #valid? deve ser true. Com uma linha de configuração no teste você consegue focar no que realmente importa: fazer a regra de negócio funcionar.

Estou criando o código de produção e meu método alvo do teste está imenso ou/e cheio de chamadas à métodos privados da própria classe

“Imenso” é subjetivo. Não deve-se encanar com a quantidade de linhas de um teste, mas sim com sua anatomia. Se ele estiver sem repetição e na ordem: input de dados, executa o método em teste, analisa resultados – seu teste estará bem.
O problema com método privado é antigo. Há aqueles que odeiam método privados e do outro lado, aqueles que usam pra tudo. Ambos estão pegando pesado ao meu ver. O método não público precisa ser bem pensado. Como você viu acima, nem sempre a responsabilidade para uma dada atividade na classe pertence à classe que você imaginou. Escalando isso para um sistema, você terá sim muitos métodos privados mal planejados. Como disse, Dr. Erskine em Captain America: The First Avenger:

O soro potencializa o que há dentro da pessoa. O bom torna-se ótimo e o mal torna-se horrível.

Aplicando uma regex s/soro/teste/ e s/pessoa/classe/, teremos uma definição hipster sobre o que é Test-Driven Development.
O TDD nestes casos, irá gritar para você de que há um problema de design ocorrendo em suas classes – e com o auxílio do teste, você consegue sair desta situação. Agora, resolver o problema é com você e seu toolbelt. Dicas:
1. Tell Don’t Ask. Como você viu acima.2. Extrair método privado(s) para Classe. Veja se faz sentido.3. Injeção de Dependência (via setter ou construtor).

Por fim, caso não tenha lido o post anterior da série, recomendo que o leia, pois há alguns detalhes adicionais aos tópicos discutidos aqui.

To be continued.
Continuação em: Parte 4 – Tópico 10 e conclusão

Rua sem saída: não consigo criar meu próximo teste – Parte II

Continuando o assunto como criar meu próximo teste, irei abordar os três tópicos seguintes do segundo post da série. Recomendo que os leiam em ordem:Continuando o assunto como criar meu próximo teste, irei abordar os três tópicos seguintes do segundo post da série. Recomendo que os leiam em ordem:
* Prelúdio* Parte 1 – Tópicos 1, 2 e 3

Estou querendo testar um método privado

Quem nunca não é mesmo? Tudo pode começar com uma inocente vontade de verificar apenas um método privado em apenas uma classe do teu software. Após pesquisar, você concluí: ah, se a linguagem de programação X permite que eu modifique a visibilidade do método para public via reflexão (parte da metaprogramação) é porque isso é bom. Será ?
O primeiro ponto a se analisar é relembrar o que não é Test-Driven Developmentspoiler: não é sobre testar se o software não tem bugs. Com a mente fresca, pode-se perceber que se TDD é uma prática de design de código que tem por objetivo guiar seu design baseando-se nos requisitos funcionais do software, por si só, TDD é contra teste de método privado.
Definição método privado por Sandi Metz: métodos privados são métodos unstable da sua API. Entende-se aqui por API todo e qualquer método público de quaisquer objetos. Os métodos privados contêm o necessário para auxiliar métodos de negócio a realizarem suas atividades e por si só, não desempenham quaisquer atividades de valor ao software.
Então porque quero testar meu método privado? Por causa de uma má definição do design da classe ou feature que você está implementando. A dica chave aqui é repensar nas suas atribuições, ou seja, será que esse método na verdade não deveria ser uma nova classe? Extração em classe aqui geralmente ocorre, então se está travado nisso, pare e repense o design do código que entorna isso.
Quer ver como isso acontece e você nem percebe? Assista essa apresentação de Michael Feathers:

Não consigo isolar a feature que vou criar. Os objetos que irão interagir com ela são complexos para mockar

Situação básica onde você se empolga codificando e quando vai ver virou Big Ball of Mud. Pode ser também que você ficou no deixa disso e não fez Test-First.
Nota para os deixa disso: não estou afirmando que Test-First é a salvação do mundo e que a paz será atingida quando todos o fizerem – meu alerta sobre Test-First é aquele aviso que seu pai/mãe te deu quando você tentou apertar um parafuso do tipo philips com uma chave de fenda, e ignorando-o(a) você seguiu. Foi doloroso, demorado e ficou mal apertado, não foi?
Geralmente, você faz mocks utilizando doubles ao invés de instanciar objetos reais para interagir com seu objeto alvo do teste. Se no teste você precisa que seu double chame um método que chame outro para no fim retornar algo, você simplesmente pode fazer seu double retornar o valor esperado do método que seu objeto em teste precisa e pronto, correto?
O problema escala quando você tem objetos métodos que não podem ser mockados. Por que isso acontece? Bem, quando você não injeta suas dependências. Veja só:

class DeixaDisso
def tdd_eh_mimimi(params)
Paypal::RestAPI.perform("/paymenow", params)
end
end

Como é que você testa o DeixaDisso#tdd_eh_mimimi se ao testar isso ele chamará de verdade o Paypal::RestAPI.perform?
Supondo que o código acima seja uma abstração aceitável, o ideal a fazer é extrair o Paypal::RestAPI e torná-lo injetável via método, por exemplo:

class DeixaDisso
def tdd_eh_mimimi(params, paypal_api: Paypal::RestAPI)
paypal_api.perform("/paymenow", params)
end
end

Com isso, no teste você pode passar um double para que responderá ao método perform e você pode fazer o mock dele criando a seguinte expectativa: __espero que seja chamado o método perform com os parametros: /paymenow e params uma vez.
Pronto. Isso é um teste de unidade baseado em expectativa muito útil e simples quando as dependências são injetáveis.
Houve uma vez que eu trabalhei com um framework PHP chamado Phalcon Framework. Quando fui fazer uns testes que envolvia Controllers, etc., descobri que ele disponibilizava os “Serviços” via di que na verdade era um container de injeção de dependência. Para chamar o serviço ‘paypal’ por exemplo, eu poderia fazer assim:

class FoobarController {
public function indexAction() {
$this->di->getService('paypal') # Objeto Paypal
}
}

Do ponto de vista do Mr. M isso é fantástico, pois você magicamente tem acesso ao $this->di que te lista todos os serviços registrados. Mas… como testar isto sem cair na real implementação dos serviços? A mesma ideia funcionava para os parametros de POST/PUT. Vinha magicamente e não injetado. Essa filosofia seguia também para os Models e ORM.

Não consigo verificar se um evento/callback do framework que estou usando foi chamado

Ora, como teste de unidade testa a unidade de uma funcionabilidade do software, deixar que o callback/evento seja chamado tornaria o teste um teste de integração, concorda?
Buscando evitar que este callback seja chamado de verdade no teste, a melhor solução é verificar se ele foi chamado dado uma condição. Em miúdos, criar uma expectativa em um mock. Como callbacks diferentes podem e são implementados de formas diferentes, não há um recipe mágico aqui, apenas a dica de que devemos garantir que ele é invocado utilizando da técnica de setar a expectativa no mock (como no item acima eu mencionei). Alguns frameworks fornecem assertions expecificos para você testar a chamada a callbaks. Por exemplo, o Rails te permite testar se um e-mail foi enviado sem enviá-lo de facto. Isso permite que você crie testes isolados para estas condições de negócio sem seguir para os mocks.
Por fim e não menos importante, você pode criar um spy no teste que te ajudará a verificar se o estado de um objeto muda após a chamada do callback. O RSpec tem assertion baseado em evento/mudança.

expect {subject}.to change {subject.will_change_x_to_y}.from(x).to(y)

To be continued.

Continuação em: Parte 3 – Tópicos 7, 8 e 9

Rua sem saída: não consigo criar meu próximo teste – Parte I

Continuando o assunto como criar meu próximo teste, irei abordar os três primeiros tópicos do primeiro post da série.

Caso não tenha visto o prelúdio que deu início a isto, recomendo que leia antes de continuar!

Não consigo imaginar em qual objeto ficará a feature – e/ou se ele precisará de outros objetos existentes para funcionar.

Antes de se preocupar com o aonde temos que nos preocupar com o o que. Explico: a vontade de sair criando coisas e ver teste passar é muito tentadora, fato. Mas antes, planejar aquela feature irá te facilitar muito o começo. Tornando em recipe:

  1. O que a feature irá fazer exatamente? Neste ponto, você já deve saber o que esta nova tarefa irá acrescentar no sistema, do começo ao fim.
  2. Será necessário criar outra classe ou é uma nova funcionalidade para uma classe existente?
  3. Essa tarefa precisará de informação de alguma classe já existente? Quais?

Carregado isto para sua memória ram (cabeça), é possível seguir em frente. O foco agora é esboçar um rascunho de como serão as coisas. Neste passo, cada um tem uma técnica diferente. Eu já fiz rascunho de UML, rasbisco em Moleskine, arquivo de texto, diagrama de sequência e arquivo de teste.

Depende do que estamos lidando, por exemplo, se você está criando uma feature que irá permitir que o sistema receba pagamentos, você provavelmente lidará com a criação de um novo módulo inteiro. Isto requer um rabisco mais arquitetural. Agora, se você está criando uma classe dentro de um módulo já existente ou apenas uma nova ação para uma classe, pode-se ficar com arquivos de teste e diagrama de sequência.

Este tipo de rascunho serve para nossa mente começar a relacionar o assunto com mais “detalhes” – este detalhe de nome, e possível relação entre objetos/pacotes facilitará pensar no assunto enquanto você está fazendo outras coisas (almoçando, por exemplo). Quem não acha que nossa mente tem background jobs? 😛

Saindo da arquitetura e partindo para o design (de código), uma coisa que me ajuda muito na hora de encontrar nomes para as classes é o Ubiquitous Language. Em miúdos, é extrair nomes/verbos das conversas com seu cliente e tentar encaixá-los dentro do software. Isso é quase uma arte – e a prática neste caso tornará você mais ágil neste passo. Se ainda assim, encontrar nome está difícil, crie a API que espera ter no código.

"Rascunho de design de código"

Durante esse processo eu acabei amadurecendo a ideia e os relacionamentos. Veja que até abortei uma ideia que ia começar. Com isso, eu tenho mais detalhes e posso sem medo criar meu arquivo de teste (RSpec, JUnit, NUnit, etc.) e começar a experimentar.

Preciso mockar um objeto do qual é criado dentro da classe que vou usar como auxiliar.

Clássico. Vou usar o esboço que fiz acima. Vamos supor que criei o código User.build_invoice(for_orders). Seguindo o que está no rabisco, sairia algo assim:

class User
  def build_invoice(for_orders)
    Checkout.new.create_for(self, for_orders)
  end
end

class UserTest < Test::Unit
  # helper method to simplify User object creation
  def subject
    User.new # Ruby doesn't require 'return statement' to return values
  end

  # collection with dummy orders just to satisfy our test
  def for_orders
    [mock('order'), mock('order')]
  end

  def test_build_invoice_with_orders
    subject.build_invoice(for_orders) # ???
  end
end

Disto eu te faço uma pergunta: como farei o teste de unidade uma vez que Checkout.new está hardcoded no método build_invoice? No exemplo acima, Checkout.new não requer nenhum objeto em seu construtor, mas provavelmente, assim como seu User ele deve instanciar outras classes internamente (o que novamente, é errado) e em algum momento seu código será intestável por causa desses objetos voodoo que aparecem do nada e você não tem como configurá-los a seu modo.

E mais, como que o User pode saber do que Checkout precisa em seu construtor? O User é bidu? Acho que não. Uma coisa interessante que existe no Ruby são os valores pré-definidos (e aceita instância de classe, viu, PHP e afins?). Eu facilmente poderia modificar meu código para:

class User
  def build_invoice(for_orders, checkout: Checkout.new)
    checkout.create_for(self, for_orders)
  end
end

Agora, User#build_invoice recebe 1 ou 2 parametros: orders e o objeto de checkout. Isto permite que você injete o Checkout na configuração que achar melhor no código. E sem mágica alguma, isto torna seu código testável novamente.

class UserTest < Test::Unit
  # helper method to simplify User object creation
  def subject
    User.new # Ruby doesn't require 'return statement' to return values
  end

  # collection with dummy orders just to satisfy our test
  def for_orders
    [mock('order'), mock('order')]
  end

  def test_build_invoice_with_orders
    checkout = mock('Checkout') # using Mocha gem to create mocks/stubs
    checkout.expects(:create_for).with(subject, for_orders).once
    subject.build_invoice(for_orders, checkout: checkout)
  end
end

Utilizei pseudo Test::Unit para ficar mais fácil para Javeiros/PHPeiros/.NETeiros, etc.

Pude criar o mock de checkout e criar uma expectativa nele que diz: espero que o User#build_invoice me chame passando os argumentos: user e orders uma vez. Para quem nunca viu, isso é um teste de expectativa sob comportamento. Tem seus usos, como já afirmou Sandi Metz.

Percebi que meu setup (before no RSpec) tem mais que 5 linhas.

Primeiramente, 5 linhas é modo de dizer. Cada projeto, cada linguagem, cada tudo, irá variar a quantidade de linhas. O ponto aqui é chamar a atenção para a complexidade de código de teste que diretamente afeta a complexidade do código de produção, que afeta o design, que afeta o bug detection, que afeta seu bug tracking, que afeta seu chefe, que te afeta e que estressa a todos. Um ciclo imenso justamente porque alguém neste meio acha que design de código não é importante e/ou que não deve ser pensado de forma contínua.

De fato a isto, a frase mostre-me seu setup e direi como tu és passa a fazer sentido. O setup é um dos caras mais evidentes nos testes. Se ele é complicado, indica que seu código é um espaguete. Apesar de gostoso na vida real, no código ele te coloca em um cenário complicadíssimo.

Soluções? Bem, refatorar ou até mesmo refazer partes/módulos do seu software. Refatorar != refazer. A conversa de não tenho tempo agora pode até ser válida em alguns casos, mas antes de afirmá-la, pense muito bem se realmente está em um momento complicado do seu negócio ou se é apenas por achar que não vale a pena.

Supondo que você pensou muito sobre o assunto e realmente não tem tempo para refazer ou refatorar, uma solução válida é criar as próximas features o mais isoladas do código macarrônico possível. Eu abordei isso no post Unit Testing em aplicações legadas e sem teste. Dá uma conferida que fará bastante sentido neste caso.

Por fim, você pode estar pecando na estrutura dos seus testes. Como disse neste post, o teste tem anatomia e você deve conhecê-la para definir se um teste está bem escrito ou não. Spoiler: não é pela quantidade de linhas que define-se isto.

To be continued.

Continuação em: Parte 2 – Tópicos 4, 5 e 6

Rua sem saída: não consigo criar meu próximo teste – Prelúdio

Você resolveu tentar a fazer test-first. Conseguiu fazer seu primeiro teste passar. As coisas estavam fluindo bem até quando você chegou em um ponto em que não consegue escrever o próximo teste. A ideia está na sua cabeça, pronta para ser jogada em execução, mas travou no processo de escrita da especificação executável (seu teste). E agora?

Neste momento, você que está ainda se adaptando a nova forma de pensar, acaba chegando em uma rua sem saída. O segredo aqui é não desistir e sair falando que o @dhh é um gênio da computação, um visionário e que TDD is dead mesmo. Calma lá, rapá!

Para lidar com este problema crítico, temos que antes analisar nosso (seu) cenário. Para tanto, vamos listar alguns sintomas que poderão nos levar a causa e depois a solução:

Ao tentar escrever o teste, eu:

  1. Não consigo imaginar em qual objeto ficará a feature – e/ou se ele precisará de outros objetos existente para funcionar.
  2. Preciso mockar um objeto do qual é criado dentro da classe que vou usar como auxiliar.
  3. Percebi que meu setup (before no RSpec) tem mais que 5 linhas.
  4. Estou querendo testar um método privado.
  5. Não consigo isolar a feature que vou criar. Os objetos que irão interagir com ela são complexos para mockar.
  6. Não consigo verificar se um evento/callback do framework que estou usando foi chamado.
  7. Tenho um mock que retorna um outro objeto que precisa também de ser configurado com mock. Parece um mock-de-mock.
  8. Vou precisar instanciar vários objetos nesta nova feature.
  9. Estou criando o código de produção e meu método alvo do teste está imenso ou/e cheio de chamadas à métodos privados da própria classe.
  10. Demorei tanto pensando numa solução que acabou desistindo e criando o código sem teste. O código de produção funciona (você o testou manualmente), porém ainda não sabe como escreverá (e se) o teste que o validará.

Prelúdio

Para começar, precisamos carregar os pré-requisitos em nossa mente para conseguir seguir em frente. Eles são leituras importantes que contêm conceitos, dicas e definições de assuntos que precisamos lembrar de bate-pronto para lidar com o grande problema acima.

  • Three rules of TDD: Red, Green, Refactor. Sem pressa. Pensar 10 minutos pode render 1h de bate cabeça e desespero com prazos.
  • Baby steps: nos seus primeiros 6 meses, recomendo que faça sempre baby steps para evitar os problemas acima.
  • God Class ou God Method – você não está abstraindo as coisas como deveria. Você pode e deve cogitar criar POROs (Plain Old Ruby Objects) que irão auxiliar seus demais objetos dentro do pacote/domínio da aplicação.
  • Tell, Don’t Ask evite perguntar algo para o objeto colaborador para baseado no resultado, fazer algo. Se precisa fazer, solicite que o próprio faça uma vez que ele tem o valor/estado que é pré-requisito.
  • Injeção de Dependência: o Ruby inteiro é baseado nisso. É algo natural e que você deve manter em seus códigos. Poderá resolver problemas de mocking e overmocking, por exemplo.
  • Crie tipos! Seu software é financeiro? Inspire-se nos nomes reais ou que seu cliente fala. Se na reunião ele diz que o cliente irá transferir dinheiro da conta pessoal para um outro cliente, experimente criar Money, Transfer, Account, Customer. Ubiquitous Language FTW!
  • SRP. The same old Single Responsibility Principle. Ok, você já ouviu isso incontáveis vezes. Mas pare para pensar naquele objeto com um método público e 10 privados. Pensou? Agora leia a dica acima de sobre criar tipos. Entendeu o que eu quis dizer?
  • Você entendeu quais os objetivos de mockar, certo?

Continuará

Vou comentar em detalhes cada um dos 10 problemas ao criar o próximo teste nos próximos posts. Vou utilizar diagramas, exemplos de código e tudo mais que for necessário em cada caso. O prelúdio obviamente contém spoilers sobre o que utilizaremos para resolver os problemas e continuar on track no test-first. Então se você estiver disposto, pense com calma e veja se consegue sair da rua sem saída antes do post que falará sobre seu problema em detalhes.

Para começar, ouça o Ruby Rogues #158 chamado Confessions e o Thoughtbot #79: The Gentle Wise One. Se você codifica em Ruby, não deixe de ler e ver o meu post sobre Arquitetura, Rails e o ecossistema Ruby, onde contém um bom start sobre as raizes de alguns dos 10 problemas ao fazer seu próximo teste passar.

To be continued.

As demais partes estão aqui em ordem:

Refatoração de código

post-10513-Code-Refactoring-Cat-in-Bathtu-yRZT

Não precisa acompanhar o “mundo dos testes” para saber o que significa refatorar. Aliás, quem nunca ouviu algum co-worker ou você mesmo tenha feito uma refatoração de um código.

Primeiro, vamos deixar bem claro o que é refatoração. Vamos lá. Você tem até o próximo paragrafo para pensar na sua definição de refatoração de código.

Pronto? Aqui vamos nós!

A milagrosa refatoração de código

Primeiramente é importante explanar de que há dois tipos de refatoração: a) refatoração, terceira etapa do Test-Driven Development (Red, Green, Refactor); b) refatoração de um código de produção já existente. Neste momento vou tratar apenas do item b).

A necessidade de uma refatoração dá-se inicialmente por uma decisão de um (ou mais) programadores sob um determinado trecho de código, por concluir de que aquilo não está construído de uma maneira aceitável. Disto, o cabra deve lembrar de que refatorar quer dizer: não mudar o comportamento da parte a ser refatorada. Apesar de óbvio, essa primeira premissa é a mais violada ao refatorar algo, pois junto da refatoração o programador resolve fazer umas coisinhas a mais (inserir novas features, por exemplo).

Não precisa ir muito longe para provar de que isso não funciona bem, não é mesmo? Ao refatorar você pode quebrar coisas. E, para evitar que essa quebra não vá parar em produção (ou parar o env de produção), você precisa de respostas rápidas a quaisquer mudanças que faça no código, por menores que sejam. (Test-First aqui, alguém?).

Item número dois: evite com todas as forças ficar criando tarefas de refatoração no projeto. A refatoração deve vir com um propósito. Prever o futuro não é um propósito. Não há necessidade de refatorar algo que está em produção há tempos só pelo prazer de refatorar um trecho de código. (você pode fazer isso em casa, para estudar, claro!) Pois você já deveria ter feito isto no terceiro passo do TDD: (Red, Green, Refactor). Se não o fez, espere até que venha precisar trabalhar com aquele código novamente para implementar uma nova feature, daí, divida essa feature em duas etapas: refatorar o código envolvido na nova tarefa e fazer a nova tarefa. Lembre-se: dois passos. Refatorar e somente depois, implementar.

O (semi)deus da refatoração

Lembra que falei que a palavra refatoração você já devia ter ouvido em algum ambiente antes? Sempre ao pegar um código é um costume a gente não entender o que aquilo faz (e por que faz). Por não entender, a gente vai e diz: ah, isso aqui precisa de uma refatoração. Será mesmo?

Esse tipo de atitude pode colocar em xeque os benefícios da refatoração e pior: quebrar algo em produção desnecessariamente, pura e simplesmente porque você sendo novo naquele projeto/equipe não entendeu o código – o que é normal em todo início. Com isso, você faz feio com a equipe e pode destruir todo um processo de amostragem e explicação sobre benefícios de uma refatoração planejada.

É importante que você vá com calma e espere até ter uma certeza mais clara das coisas.

Refatoração nem sempre é a melhor saída

Outro ponto a favor de evitar sair refatorando sem saber quem nem porquê é ter o controle analítico de analisar o cenário em questão e verificar se a refatoração de código solucionaria o problema. Já vi casos em que o código em si não estava ruim, estava aceitável, o verdadeiro problema era um design mal pensando e neste caso, a refatoração não ajudaria em nada. Você precisaria ir além nas decisões.

É preferível manter um código espaguete por mais um tempo do que perder a (única) chance de mostrar os benefícios. No podcast de número 157 do Ruby Rogues, Rebecca Wirfs-Brock afirmou outro ponto importante:

“You’re sort of arguing that refactoring is not necessarily always the best way to clean up a design. Sometimes, you might want to start over.”

Às vezes realmente vale mais a pena fazer um git reset ou um git stash (como disse o Avdi Grimm brincando com ela) mental e partir a fazer uma outra solução do zero. Já tive que fazer isto inúmeras vezes ao longo do tempo. Um grande aliado nesse processo são os Testes de Unidade com Test-First, pois eles dão uma resposta muito rápida sobre seu progresso (ou estagnada) de raciocínio.

Hoje a conclusão, dar-se-á em forma de resumão!

Resumão:

  1. Respostas rápidas ao refatorar. Teste de unidade é a única forma de conseguir isto rapida e isoladamente.
  2. Refatorar. Depois de pronto, volte para implementar o que ia fazer no começo. Não faça os dois juntos por mais que Goku desça da núvem para te pedir isto.
  3. Refatorar ao entrar num projeto: it’s a trap!
  4. Às vezes é melhor um git reset mental e partir para outra solução.
  5. Estar apoiado em teste de unidade e test-first trará a confiança necessária para tomar a decisão de refatorar.
  6. Esforce-se para não tornar a refatoração uma task do seu projeto. Ela deve ser junto de uma task de implementação. Refatorar precisa de propósito.
  7. Red, Green, Refactor (TDD) != Refatorar código de produção.

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 😉