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? :P

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

Hélio Costa

Desenvolvedor de software há um tempinho. Interessou-se por Object-Oriented Design e Test-first antes do Eddy Merckx ser removido na organização da UCI Road World Championships.

Sao Paulo, Brazil