Hanami em prod: 2 anos depois – Parte 1

Começamos os protótipos quando o projeto se chamava Lotus-rb. Lá em Outubro de 2015, a ideia era validar se quem-sabe-talvez, Lotus-rb pudesse ser uma alternativa real ao então nosso principal projeto em Ruby on Rails.

Le Prototype

Como todo mundo, fizemos um ToDo app para validar. Experimentamos o model e o validation, pois já tinhamos em mente separar o Input e sua validação do domain-model. Queríamos isolar ao máximo nosso modelo, muito nos moldes do Clean Architecture do Uncle Bob / Ports & Adapters do Alistair Cockburn. A ideia era muito experimental e confiávamos na robustez por baixo do Lotus-rb: Sequel. O Sequel, diferente do Lotus-rb, era estável e bem conhecido no ecosistema Ruby. Pensavamos: se o Lotus cair, ficamos com uma camada de abstração do Sequel e migramos isso depois facilmente para um Sequel-model da vida.

Meses depois, nosso projeto Core foi ao ar, substituindo partes do nosso agora legado Rails app. Já com o nome de Hanami e algumas atualizações feitas, tudo ia bem, para nossas necessidades da época. O projeto é uma API com interface JSON API, com foco em separação em camadas, Domain-Driven Design e arquitetura Hexagonal.

Le Presentation Layer

Ao longo deste processo, testamos outras gems como Roar (do Apotonick), Representable, JSONApi-Serializers e Grape. Como dá para ver, nossa Presentation Layer demorou a ficar do jeito que imaginávamos. É a vida. Ela é assim e boas. Fizemos uma versão básica da Presentation Layer e depois de tudo pronto e algumas utilizações, repensamos como poderíamos deixa-la de facto isolada.

Nossa principal questão foi como lidar com a transformação do Domain-Model para entregar algo “burro” ao nosso Presentation Layer. Outro ponto era o ponto inverso: como transformar o payload do Request em um objeto de transferência para ser encaminhado a Application Layer – a.k.a Use Cases. Idas e vindas, começamos com um modesto fluxo endpoint -> form object -> application service -> domain model até chegarmos em algo com responsabilidades melhor definidas:

# Presentation Layer
endpoint
  form object
    optional triggers: Validation & Relationships (JSON API specifics)

# Application Layer
  use case
    orchestrates to:
      domain model or # domain model
      infrastructure service # application boundaries

Antes de ir para production, tivemos que refazer nossa ideia de Form Object, pois o modelo inicial, não continha uma forma plugável para adicionar Input Validation (Hanami Validation implementation). Era muito manual e repetitivo. Natural neste ponto do processo, pois tivemos que distribuir nosso foco de atenção em toda a arquitetura inicial.

Com o novo modelo, o FormObject delega para a validação usando Template Method, caso o tal FormObject dê include na Validation que deseja utilizar naquele endpoint. Isso deixou muito transparte como validamos as coisas, pois no final, tinhamos uma delegação muito boa entre as classes FormObject e Validation.

Como spoiler, se liga um exemplo de FormObject:

# apps/web/form_object/auto/financing/applications/patch.rb

require_relative 'validations'
require_relative 'relationships'

module FormObject
  module Auto
    module Financing
      module Applications
        class Patch
          include Validations
          include Relationships

          def initialize(params, current_user, overrides = {})
            @raw_params = params
            @current_user = current_user
            @update_application = overrides.fetch(:application_use_case) do
              ::UseCase::Auto::Financing::Application::Update.new
            end
            @repository = overrides.fetch(:applications_repository) do
              ::Auto::Financing::ApplicationsRepository
            end
          end

          def process
            validate_using(@raw_params)
          end

          private

          def validation_succeed(validated_params)
            @update_application.update_application_form_for(
              @current_user,
              entity_with_relationships_updated,
              validated_params
            )
          end

          def entity_with_relationships_updated
            update_relationships(find_application(@raw_params['id']))
          end

          def find_application(id)
            @repository.find(id)
          end
        end
      end
    end
  end
end

# apps/web/form_object/auto/financing/applications/validations.rb
# Classe de Validation citada acima

require 'form_object/definitions/validatable'
require 'validations/auto/financing/application_validator'

module FormObject
  module Auto
    module Financing
      module Applications
        module Validations
          include Definitions::Validatable

          def validator_class(params)
            ::Validations::Auto::Financing::ApplicationValidator.new(params)
          end
        end
      end
    end
  end
end

# apps/web/validations/auto/financing/application_validator.rb
# Hanami Validation class

module Validations
  module Auto
    module Financing
      class ApplicationValidator
        include Hanami::Validations

        attribute :requester_full_name, type: String, size: 0..200
        attribute :requester_cpf, type: CoerceTypes::Cpf, size: 11
        attribute :status_flow, type: String
        attribute :purchase_value, type: CoerceTypes::Nullable::Money
        attribute :down_payment, type: CoerceTypes::Nullable::Money
        attribute :loan_amount, type: CoerceTypes::Nullable::Money
      end
    end
  end
end

Le Application Layer

Nosso próximo desafio foi mudar nossos Application Services. Trocamos o nome de Application Service para Use Case. Isso por si, já deixou objetivo o que era esperado destas classes bem como da Application Layer inteira.

Use Case bem descrito passou a ser prioridade em meados de maio/2016, quando percebemos que a Application Layer precisava sair de um mero detalhe, para assumir a responsabilidade de orquestrar como nossos casos de uso eram controlados. Nosso objetivo era simples: conseguir com um tree boundaries/use_cases entender o que o software é capaz de fazer. Todo aquele papo de sempre sobre nomenclatura de classes, etc., mas ir além e pensar nos contextos (modules/namespaces) que os Use Cases fazem parte.

Como todo o projeto, nossos Use Cases são stateless e recebem suas dependências através de injeção de dependência no initialize. Podem controlar db transactions e em alguns casos, disparar Domain Events, como sugerido pelo Vaugh Verner em seu livro Implementing Domain-Driven Design.

No próximo post, vou descrever como foi lidar com a evolução de nosso domain-model e como conseguimos lidar com isso ao longo destes dois anos.

Talk: Lidando com defeitos em Test-Driven Development

Em Setembro/2014, participei do evento #S7TDD (7 dias com Test-Driven Development). O evento foi muito proveitoso com excelentes reviews dos participantes.

Falei em algumas oportunidades, uma delas abordei como o Test-Driven Development poderia auxiliar no gerenciamento de “guerra”, ou seja, quando as coisas “dão ruim”. Abaixo, o vídeo completo da minha apresentação:

Lidando com defeitos em Test-Driven Development from Hélio Costa e Silva on Vimeo.

Foi uma experiência e tanto participar de um evento online como o #S7TDD. Quem sabe em breve eu não aparece em outro 😉

Planejando no Macro para entender o micro

Finalizei o último post deixando um suspense no ar. Se não viu, recomendo que leia! Pois bem, quando falei sobre planejar no macro e codificar no micro, deixava ali uma ideia útil de fato, muito bem abordada, que quase todo developer já ouviu pelo menos falar o nome e que porém, já foi muito mal utilizada.

A UML, pode ser um aliado forte no seu planejamento macro de uma funcionalidade do software. Calma! Nada de fechar a aba e ir me xingar no twitter. Estou afirmando para você que é desenvolvedor e cria seus próprios testes a esboçar numa visão macro o que planeja fazer e com a UML, você obtém esse rascunho rapidamente.

Porque a UML e não (coloque aqui seu outro método) ?

UML é uma linguagem de modelagem de diagramas e sua simplicidade (no uso simples dela) a torna uma vantagem no esboço de rascunhos macro de seu software. Outro ponto a favor da UML é o shareable que ela permite: mande-me um esboço UML que eu entenderei o que você quer me falar.

Esboço!

Não estou falando para chamar o arquiteto de software para ele criar todo o software em UML e mandar você fazer. Vou repetir: crie você seus esboços para ajudar no seu próprio entendimento do que pretende e como fará os relacionamentos. Geralmente, eu faço micro esboços que resolve uma e somente uma funcionalidade. Após criá-la, consigo ver se fará sentido aquela minha ideia do ponto de vista arquitetônico e daí então, crio meu teste de unidade e sigo adiante.

  • Eu não faço uma funcionalidade e depois ligo com outra que liga com outra;
  • Não guardo estes rascunhos depois que acreditei na solução macro e segui adiante com a micro (Teste de Unidade) ajustando meu design de código, e;
  • Não faço rascunho macro em UML para toda e qualquer nova feature. Apenas naquelas onde as fronteiras da classe alvo do teste estão meio obscuras para mim.

Isto quer dizer que se Order se relaciona com alguns atores (outras entidades/classes puras) e este relacionamento está estranho para mim, eu tenho subir o nível de abstração, saindo do micro e indo para o macro, buscando entender como dar-se-á o futuro relacionamento ou/e se ele faz mesmo sentido naquele contexto.

Tipos de macro-esboço

Geralmente eu apelo para o Diagrama de Objetos por ser algo simples: coloca dentro do retângulo o nome da classe e pronto. Onde ele por ser útil:
a) quando você está procurando quais as fronteiras seu objeto terá; (lembre-se de que todo e qualquer objeto deve ser planejado como se o mesmo fosse uma API provendo serviço através de métodos públicos a outros objetos do mesmo sistema – por isso uso muito o termo fronteira [boundary]).
b) quando você precisa entender se o objeto deverá ser Aggregate Root de algum outro via composição.
c) traçar fluxos possíveis dentro de um conjunto de objetos.

Quando a coisa aperta e eu fico desconfiado das responsabilidades de cada método em objeto(s) em uma feature, eu recorro ao Diagrama de Sequência. Ele torna-se muito útil nos cenários macro-micro (nome horrível), onde estou planejando o macro, mas entrando um pouco no detalhe da responsabilidade (SRP, alguém?) da feature. Ele te ajuda a lembrar sempre da Lei de Demeter com seu Tell Don’t Ask.

Já acabei utilizando raramente o Diagrama de Classe, onde rascunhava um pouco sobre os métodos de uma classe, mas no meu uso ele acabava não agregando na visão macro, por especular a visão micro sem conhecimento de causa.

Porque não usar a UML como documentação de software?

Quer tentar? Vá em frente (e quebre a cara). Eu e alguns amigos já tentamos manter um Diagrama Caso de Uso em um software anos atrás e sinceramente? Totalmente inútil. Precisava de atualização a todo instante; Ficando desatualizado tornava-se um peso a mais e não ajudava no design pois era um punhado de diagrama e não código executável que te prova que sua teoria no código de produção funciona ou não. Imagina você tendo que atualizar todo diagrama de sequência assim que resolve mudar o input ou output de um método. This is madness! – além de ser esforço inútil pelos motivos já citados (não exercita o código de produção).

Concluindo

Diagramas UML, rabiscos, círculos interligados, etc., ajudam você a planejar sua visão macro quando sente-se desconfortável com certa integração entre objetos. São valiosas ferramentas para esboçar sua ideia e reorganizar sua mente antes de seguir para seu próximo Teste de Unidade, que diferente do primeiro, exercita código de produção como ninguém.

Como manter sua suite de testes saudável

taporra

Depois de uma longa pausa por causa da demanda que o #S7TDD me gerou, retorno das sombras para continuar com as polêmicas do Test-first. Hoje, retomo com algo primordial, principal, essencial, o must have em Test-first: nada menos do que ele, a manutenção da suite de testes.

O que é a manutenção da suite?

Partindo do princípio de que sua suite está em constante crescimento, você uma hora ou outra irá se perguntar em como organizar a casa para evitar se perder e não se enrolar com seus testes de unidade.

Para tanto, a manutenção da suite é importante e, quanto mais você praticar Test-first, mais simples e eventual torna-se-á tal manutenção.

E para que preciso saber/fazer isso?

Como nem todo mundo está no nível de Robert Martin (@unclebob), se reorganizar torna-se importante para continuar vendo resultado no Test-First e principalmente, continuar entendendo como o software funciona só de olhar um punhado pequeno de testes de unidade.

Perder o controle da suite é relativamente fácil principalmente quando estamos começando com testes de unidade, pois como muitos questionam: “mas e as junções? você não une as coisas nunca?”. A vantagem do Teste de Unidade mora justamente no cerne apontado nesta frase, mas como tudo tem um viés, você precisa estar atento justamente com a manutenção deste monte de teste de unidade espalhados pelo seu /specs.

Sejamos pragmáticos aqui: sua suite está precisando de uma reorganizada quando você lê um teste e não entende o que aquilo está fazendo dentro de um contexto macro. Deixando a explicação fancy de lado, temos:

  1. Pegue um teste de unidade aleatoriamente;
  2. Leia e veja o que ele implementa (funcionabilidade);
  3. Tente reproduzir aquela situação em um Controller, Command, etc, ou seja, suponha que você viu o teste e deseja criar a funcionalidade numa Action de Controller qualquer;
  4. Ache uma outra funcionalidade que se relacione com essa primeira que você reproduziu;
  5. Leia o teste dela, tente reproduzi-la ligando a primeira funcionalidade na segunda.

Conseguiu entender ambos os testes e relacionar as funcionalidades? Parabéns, sua suite está saudável (por enquanto haha).

Restabelecendo a ordem na casa (dos testes)

A primeira coisa a se fazer caso os steps acima não tenham dado resultado é verificar a anatomia dos testes de unidade. Às vezes, você acumulou duas ou três funcionalidades em um mesmo teste de unidade sem perceber. Ao splitar estes testes em testes específicos para cada situação, a legibilidade irá saltar de um espaguete para algo compreensível. Costumo dizer que esta é a principal dica para quem quer arrumar a legibilidade dos testes.

Supondo que o problema da anatomia está resolvido, podemos melhorar a legibilidade seguindo o Ubiquitous Language descrita por Eric Evans em seu renomado livro Domain-Driven Design cujo já o mencionei aqui algumas vezes no passado. Dar nomes é algo complicado (veja pelas ruas das cidades). O nome é a coisa mais importante do seu código. Isto é o elo entre o código e a vida real, solicitada e falada o tempo todo por alguém de business. Quanto mais você incorporar os nomes que ouve no código, mais natural ficará as reuniões de planejamento/sprint.

Nosso item número três do check-list é o Princípio da Responsabilidade Única (SRP, sigla em inglês). Lê isso: seu método em teste precisa fazer apenas uma coisa. Ele pode entretanto, se relacionar com outros objeto#metódo para complementar sua atividade, pedindo uma espécie de suporte. Pode também delegar atividades para outros objeto#método. Pode inclusive, se passar como parâmetro para outros objeto#método.
Veja a quantidade de recursos em Design Orientado a Objetos que você tem a disposição para não violar a SRP e você aí fazendo 3, 4, 5 steps em um único método sem a menor necessidade. Vai entender, né?

Sério. SRP é TOP 3 não é por acaso. Ela é aqui na minha lista a primeira que trata de código e OOD. As outras duas são mais arquiteturais e convenções do que código na prática.

Acredito que estas três dicas farão o trabalho de reduzir o caos que sua suite possa estar enfrentando enquanto lê isso. Um chute nada estatístico seria 80~90% de problemas resolvidos.

Como evitar on the fly que isso aconteça?

Planeje no macro; Codifique no micro. Não entendeu? Segunda-feira que vem você entenderá o que isso quer dizer.

Destaque

Existe vida após o primeiro teste passar?

listen-to-tests

Conseguiu fazer seu primeiro teste passar. Está orgulhoso de seu novo achievement porém sabe que esse primeiro passo é apenas o início para conseguir mensurar uma evolução no seu processo de desenvolvimento usando Test-first como referência de design de código. Agora, tudo que você precisa é continuar neste ciclo.

O ciclo é importante para evitar se atrapalhar e perder o ritmo neste começo. Fato! Mas, ainda mais importante é ter uma ideia clara do que pretende fazer a seguir.

Saindo um pouco de Test-first, uma coisa legal que você pode fazer é ter uma ToDo list. São aquelas coisas simples onde você organiza suas tarefas. Vai de Remember The Milk até um Trello ou Pivotal Tracker . Obviamente cada um deles tem seus pontos de ataque, mas tangenciei o tema justamente para te alertar que uma mente organizada, ajuda e muito você manter um ritmo. Minha técnica é bem simples: ao final do dia, eu vejo meu progresso no software e estimo um novo ponto de parada para o próximo dia. Quando o dia chega, eu tenho em mente que meu objetivo do dia é “Conseguir enviar um Pedido com sucesso”, por exemplo.

Com a organização em dia, você sabe o que atacar agora. Assim sendo, podemos voltar ao Test-first exclusivamente.

Vida após o primeiro teste com Test-first

Qual a próxima feature importante do seu negócio? Veja, entenda como feature requisitos funcionais e não funcionais, ou seja, pode ser algo não necessariamente explicito pelo chefe/cliente, mas algo que você percebeu fazendo seu teste de unidade com Test-Driven Development. Acredite: isso acontece e muito!

Criar cenários sem final feliz é igualmente importante ao cenário feliz. Se você espera que seu código some dois números, o que acontece se você mandar dois null para ele somar? Ele deveria fazer algo? Se é importante que ele seja resiliente e lide com este tipo de coisa, você deve criar um teste de unidade para este cenário também!

É perfeitamente aceitável criar um teste de unidade que dado dois null ele deverá retornar uma exceção (Exception). O anormal, seria você criar um teste de unidade que não espera que uma exceção seja lançada. Neste caso, você deve esperar um valor de retorno ou que um mock seja chamado.

Muito interessante criar testes que exercitam diferentes cenários e configurações de seus inputs. Por exemplo, numa inscrição, você pode simular o comportamento do método que inscreve pessoas, utilizando pessoas cadastradas, pessoas elegíveis à inscrição e pessoas não elegíveis a inscrição. Desta forma, você teria testes assim:

it “fará a inscrição dado um usuário elegível”

it “não fará a inscrição ao receber um usuário expirado”

it “não fará a inscrição ao receber um usuário inelegível”

Na maioria dos casos, você pode perfeitamente possuir mais de um caso de teste para um mesmo requisito funcional que você pegou para fazer.

O código mudará

Acostume-se. Ele mudará! No seu primeiro teste você não tem nada no seu domínio. Após o primeiro teste, novas coisas surgirão. No segundo teste, você acabará mudando código de produção que afeta o primeiro teste. Isto é ruim? Muito pelo contrário: isto é excelente. Significa que você está literalmente Guided by Tests, pois está criando o código necessário para seu teste passar. Está com isso deixando o Test-first guiar seu design de código.

Problemas irão ocorrer. Em problema, o primeiro passo é não seguir adiante. Mantenha-se aonde está, exatamente no teste que você está com dificuldades. Tente entender o que o teste está lhe dizendo. No mês de agosto inteiro, dediquei os posts a tratar os problemas mais comuns ao tentar seguir adiante com TDD. Leia o Prelúdio da série “Rua sem saída” e veja se eu abordei sua dúvida. Isto te ajudará a voltar aos trilhos (Rails ;P) com Test-first.

Concluindo

Organização mental; Seguir passos palpáveis que dão retorno rápido em modificações; Lembrar-se do Red, Green, Refactor; Efetivamente fazer o teste antes do código de produção, pois o teste te auxilia a guiar o design do código; saber “ouvir” o teste; e, o mais importante: não ter medo de mudar seu código de produção devido a um novo teste de unidade são a chaves necessárias para abrir qualquer caminho travado.

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