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