TDD para código de infraestrutura? Acho que não…

Standard

Após iniciar um texto sobre a importância de testes automatizados para código de definição de infraestrutura, refleti sobre o uso de TDD nesses casos e cheguei a uma conclusão um pouco polêmica.

Queria deixar claro que esse texto é propositivo e aberto a mudança de opiniões com base nas interações. Meu objetivo é iniciar esse debate, apresentado as minhas conclusões sobre esse assunto e assim  possamos juntos concluir algo coletivamente sobre isso.

Antes de pular para conclusão, vamos primeira contextualizar o que é TDD. De acordo com o nosso “nerd alfa” Martin Fowler:

“Test-Driven Development (TDD) é uma técnica para construção de software que o processo de desenvolvimento através da escrita de testes.”

De acordo com esse artigo, é uma prática que força o desenvolvedor a pensar sobre seu código antes de ele escrever ele e que TDD é muito mais sobre design do que teste. (Dica do Luis, no comentário).

Em essência você segue dois simples passos repetidamente:

  • Escreva um teste para validar a funcionalidade que precisa adicionar
    • Exemplo: Quero uma função para somar dois números, eu escrevo um teste que a soma de 2 + 2 o resultado seja sempre 4. Qualquer resultado diferente desse indica que a função está errada
  • Escreva de forma imperativa o código até ele passe nos testes
    • Exemplo: Você escreve o código que recebe dois número como parâmetros e soma.

Seguindo essa lógica é fácil pensar que seria ideal isso acontecer também para infraestrutura, certo? Não necessariamente.

Código de infra geralmente é descritivo e não imperativo

Nos exemplos demonstrados acima, temos, a partir de um único teste, uma grande gama de possibilidade de implementação da função a ser testada, ou seja, para um teste de soma de dois números, o código resultante para satisfazer essa validação pode variar bastante.

Desenvolvimento de software normalmente utiliza o modelo imperativo de construção, ou seja, você precisa dizer detalhadamente de que forma ,o que você deseja, seja feito. O código de infraestrutura regularmente segue uma outra linha, que é a descritiva. Você apenas precisa dizer o que deseja e não precisa implementar como isso será executado.

Comparação entre código imperativo e descritivo

Porque não TDD

Levando em consideração que um dos motivadores do TDD seja o auxilio na construção da lógica por trás da solução do teste em questão e no modelo descritivo a lógica é algo raro, fazer um teste para cada definição de infraestrutura me parece um trabalho desnecessário, já que o código de teste seria bem parecido com o usado para atender o teste.

Exemplo:

Quero criar o usuário elmo e o teste seria criado da seguinte maneira com o serverspec

describe user('elmo') do
 it { should exist }
end

O código de infraestrutura para atende o teste acima seria:

user { 'elmo':
    ensure => present,
}

Você há de concordar que esse teste nunca falharia, uma vez que não precisei de nenhuma lógica aqui. Sem falar que eles são bem parecidos na sua escrita, ou seja, no final de contas seria um grande desperdício de tempo.

E se meu código tiver lógica, faz sentido TDD?

Tecnicamente falando, sim, mas na prática nem toda definição terá lógica e ter isso como metodologia de desenvolvimento talvez atrapalhe mais do que gere algum valor para seu processo de desenvolvimento.

Isso quer dizer que não devo ter teste no meu código?

Claro que não! Testes automatizados são bem importantes para a confiança do seu código. Não vou me aprofundar nas razões de ser fazer teste, pois isso foi bem explicado aqui.

Atualização pós comentários

Depois de uma boa interação nos comentários, vale a pena deixar claro algumas coisas que talvez tenham ficado meio solta no artigo e adicionar outras que faltaram no texto ou estavam de forma incorreta.

TDD somente com teste unitário?

A ideia que é TDD seja orientado a teste unitário não foi proposital, mas no caso de teste de infraestrutura esqueci de comentar que testes de integração e outros mais complexos demandam interação com uma infraestrutura provisionada, ou seja, o feedback é bem lento, o que inviabiliza (em minha opinião) o trabalho orientado a teste.

É importante que você crie seu ambiente “do zero” a cada teste, para que você garanta que não tem dado de outro teste ou alguma modificação fora da definição no ambiente. Com isso em mente, é comum encontrar situações que podem demorar até 30 minutos para reconstruir o ambiente, aplicar as definições e então executar os testes.

Se você for utilizar testes de integração ou outros mais complexos pra guiar seu processo de desenvolvimento, ter esse tempo de espera a cada interação “Red-Green-Refactor” seria complicado para o processo de escrita de código.

 

  • Jairo Junior

    Quando você realiza um teste para uma função add(a, b), você quer garantir que ela executa uma soma de dois inteiros, mas você não está preocupado se o operador “+” funciona naquela linguagem.

    `user` é um resource type nativo do Puppet e de forma análoga, você não quer testar se ele realmente faz o que se propõe, e sim trabalha com a premissa de que ele funciona, afinal, já foi testado (https://github.com/puppetlabs/puppet/blob/master/spec/unit/type/user_spec.rb https://github.com/puppetlabs/puppet/tree/master/spec/unit/provider/user)

    Ou seja, você não testa o código de terceiro, e sim se o o seu código funciona em conjunto com código de terceiros. O que você quer testar no fim das contas é se o seu módulo está utilizando `user` para alcançar o seu objetivo.

    Seu post levanta uma questão muito pertinente: é muito difícil separar o que é unitário, integração e aceitação. Puppet é uma DSL e a menor unidade para teste seria garantir que está utilizando esta DSL de forma adequada.

    Qual a abstração do Puppet para um node? Coleção de recursos [relacionados] que representam o estado desejado daquele node – o /catálogo/.

    Qual o objetivo do meu teste então? Garantir que o catálogo gerado para o node possui a coleção de recursos que desejo com o estado e ordem apropriados. Uma ferramenta para este propósito seria: https://github.com/rodjek/rspec-puppet

    O serverspec verifica o estado /real/ de um server, ou seja, ele possui mais valor, mas também é mais lento, caro, e frágil, conforme pirâmide do outro post. Não o vejo como uma ferramenta apropriada para o propósito de testes unitários, estararia mais próxima dos objetivos de integração/aceitação.

    E quanto ao TDD? Você esqueceu de trazer a essência do mesmo do artigo original: “Refactor both new and old code to make it well structured.”

    O mais importante do TDD é melhorar o design do seu código baseado nos feedbacks que o seu testes lhe dão. Se seus testes estão complexos é um sintoma que seu código está complexo. E por ter uma rede de segurança (testes), essas mudanças/refatorações são menos arriscadas.

  • Luis

    Lembre que TDD é sobre design de software não sobre testes automatizados (http://www.drdobbs.com/tdd-is-about-design-not-testing/229218691).

    Ainda assim o artigo é interessante: acreditamos que há pouco valor em testes unitários (ou pequenos) para linguagens declarativas.

    Mas lançaria um desafio, que tal fazer um TDD/BDD para infraestrutura escrevendo o monitoramento antes da infraestrutura? Isso faz sentido? Deve ter alguns pros/cons. Acho algo interessante pra discutir.

    • O problema no monitoramento como TDD é o fato de ser assíncrono, ou seja, eu precisaria esperar o polling do monitoramento.

      Isso sem falar que subir uma infra de monitoramento é bastante custosa. Eu ainda não vejo muito ganho nisso, mas posso estar errado.

  • Juraci Vieira

    Tenho alguns pontos a observar sobre o TDD.

    1. O teste deve ser agnóstico da implementação de sua solução.
    O fato de o código que satisfaz a especificação ser parecido descritivamente não torna o processo de construção outside-in desnecessário. A especificação (teste) deve ser agnóstica de como a mesma é satisfeita, ou seja, não importa se você usou paradigma descritivo, imperativo ou orientado a eventos, o que importa é que o usuário X existe depois que a sua solução “rodou”.

    2. “TDD não é necessário se não há lógica a ser implementada” (foi o que extraí da sua colocação, me corrija se eu estiver errado)
    Acredito que a abordagem de desenvolvimento outside-in traz muitas vantagens além de ajudar a guiar o design do código incrementalmente. Aqui vão algumas:
    – Forçar situações de falha em sua aplicação/automação (RED). Para mim, conhecer como a minha aplicação falha em diversos níveis é extremamente importante. O TDD me força a ver essas situações a todo o tempo fazendo com que eu espere por um dado estado obtenha outro diferente.
    – Evoluir o design somente o suficiente para atender a uma dada especificação (teste). Com TDD você acaba adquirindo a disciplina de não fazer mais do que o estritamente necessário para satisfazer uma dada expectativa. Você tem um foco em uma dada especificação e não fica imaginando situações e caindo em “over-engineering”.
    – TDD não tem como objetivo criar testes mas os mesmos surgem como um feliz efeito colateral. Os testes gerados pelo TDD permitem com que você tenha segurança em, por exemplo, trocar Ansible por Puppet (credo hahaha) desde que após isso sua aplicação ainda satisfaça as expectativas dos testes.

    Sobre a utilidade do TDI (test driven infrasctructure) eu concordo que em muitos casos é exagero. Vejo o TDI como sendo um exercício interessante para quem quer aprimorar os conhecimentos em teste de infraestrutura e automação em geral, porém o fim prático disso é um tanto quanto questionável. Eu concordo com seu questionamento mas discordo quanto aos motivos pelo qual você descartou o TDD.

    • Obrigado pela contribuição Juraci! Vão aqui alguns colocações sobre seu comentário:

      “Forçar situações de falha em sua aplicação/automação (RED). Para mim, conhecer como a minha aplicação falha em diversos níveis é extremamente importante.”

      Perceba que infraestrutura é um pouco diferente de aplicação. O código é de definição e não imperativo para especificação de tarefas, sendo assim, eu não preciso testar comportamentos, eu preciso checar estados (normalmente).

      “Evoluir o design somente o suficiente para atender a uma dada especificação (teste)”

      Quando está usando código descritivo, tal como ansible e afins, não há como mudar muito o design, pois isso está dentro do código da ferramenta de gerência de configuração.

      “TDD não tem como objetivo criar testes mas os mesmos surgem como um feliz efeito colateral. ”

      Isso está mais relacionado aos testes em sí, do que ao TDD. Eu não sou contra aos testes, apenas tenho dúvida da eficácia do TDD para infra.

      Acho que no final de contas concordamos que não é muito eficaz fazer TDD pra infra 🙂 Que bom!

      • Juraci Vieira

        “Perceba que infraestrutura é um pouco diferente de aplicação. O código é de definição e não imperativo para especificação de tarefas, sendo assim, eu não preciso testar comportamentos, eu preciso checar estados (normalmente).”

        O fato de um usuário ter sido ou não criado é um estado interessante de ser verificado, independente do paradigma que você esteja utilizando. O benefício que o TDD proporciona de fazer com que você se depare com falhas em sua aplicação/automação ainda assim é interessante, pois elas te levam a declarar o que você necessita em sua ferramenta, e entender passo a passo o que você precisa fazer para ter sua infra “de pé”.

        “Quando está usando código descritivo, tal como ansible e afins, não há como mudar muito o design, pois isso está dentro do código da ferramenta de gerência de configuração.”

        Vou ter que discordar aqui, você não tem design apenas quando tem código. Eu posso ter um design orientado a Playbooks “puros” ou a “Roles” no caso do Ansible. O design tem mais haver com a arquitetura da sua solução como um todo que pode ser código ou uma abstração mais alto nível como no caso do Ansible.

        • “Vou ter que discordar aqui, você não tem design apenas quando tem código. Eu posso ter um design orientado a Playbooks “puros” ou a “Roles” no caso do Ansible.”

          Não acho que isso seja design do código. Isso é escolha da arrumação dos códigos, que no final de contas seria o mesmo como resultado, apenas a arrumação que mudaria.

  • Waldemar Neto

    Eu não fiz grandes coisas na prática mas tenho estudado e testado bastante. Comecei com esse livro: http://shop.oreilly.com/product/0636920020042.do (Test-Driven Infrastructure with Chef) ele fala de diversos pontos sobre testar infraestrutura e foca bastante no ponto que o @juracivieira:disqus falou anteriormente sobre “outside-in” que é a ideia de começar por integração ou aceitação e ir chegando nas unidades conforme o design vai sendo guiado. Um resumo desse livro ta disponível nesses slides: https://speakerdeck.com/atalanta/applying-tdd-to-infrastructure-code

    Também um ponto que eu vejo sempre quando se fala de TDD não só de infra mas de software, é o lance de dizer que TDD é teste unitário. TDD é guiar o desenvolvimento baseado em testes, sendo esses aceitação, integração, ou qualquer outro. Começar de fora pra dentro, ou seja, por um teste de integração por exemplo deixando assim para que as unidades sejam testadas quando elas realmente aparecerem. Acho que essa imagem resume o que eu quero dizer: https://github.com/ror-study-group/cucumber-blog/blob/master/README.md#bdd-cycle

    Deixo a dica de livro aqui: https://pragprog.com/book/achbd/the-rspec-book

    Parabéns pela iniciativa de lançar a discussão

    • Obrigado pelo comentário Waldermar, vou fazer recortes do seu comentário e responder:

      “que é a ideia de começar por integração ou aceitação e ir chegando nas unidades conforme o design vai sendo guiado.”

      “Também um ponto que eu vejo sempre quando se fala de TDD não só de infra mas de software, é o lance de dizer que TDD é teste unitário. TDD é guiar o desenvolvimento baseado em testes, sendo esses aceitação, integração, ou qualquer outro. ”

      O problema é a velocidade no feedback! teste de aceitação e integração requer que a instancia esteja ligada. Sendo assim precisarei muito tempo para rodar um teste e não funcionaria bem meu TDD, né? Minha etapa de desenvolvimento demoraria bastante 🙁

  • Rogério Chaves

    Olá!

    Parabéns pela coragem de levantar a polêmica ahahaha

    Eu entendo o que você quis dizer, e sinto algo parecido trabalhando com código descritivo, por exemplo, com Haskell ou Elm, tenho dificuldades pra escrever teste pra funções puramente descritiva, muitas vezes o sistema de tipos já garante quase tudo pra mim. Já quando a função tem lógica, aí sim faz mais sentido, como você disse, e faz mais sentido inclusive fazer QuickCheck

    O caminho que encontrei, como o pessoal disse e eu reforço, é subir um nível acima, escrever o seu teste de integração primeiro, e partir daí, são nesses testes que eu vejo mais valor, porque eu já tenho garantias que as funções separadas estão funcionando bem, mas não em conjunto. Você ainda vai estar fazendo TDD, só que não tanto no nível unitário, que é o que naturalmente se pensa ao falar TDD.

    Até!

    • Obrigado pelo comentário! A ideia agora é praticar TDD com teste de integração. Logo posto novidades! 🙂

  • Guilherme Oki

    Parabéns pelo tópico, é bem maneiro levantar mais sobre esse assunto. Eu particularmente gosto de TDD para infra porque ele te obriga a pensar mais no que a sua infra necessita de verdade, deixando ela até mais enxuta. Além disso, outro benefício que eu vejo em fazer os testes primeiro é quanto a cobertura, por mais que eu consiga fazer isso sem TDD, com TDD normalmente consigo fazer mais testes e obter ter uma cobertura maior. Claro que isso pode ter um custo, visto que alguns testes de infra são bem pesados. Concordo que código infra que envolve lógica são bem mais úteis serem testados usando TDD e pra quem tá começando com testes e com infra TDD ajuda bastante. Pode ser que dependendo do seu nível de experiência em infra pode até não agregar muito.

  • kcfelix

    Me confunde um pouco essa premissa que parece estar implícita no teu post de que TDD precisaria ser feito com testes unitários. Como os comentários por aqui demonstram, já é bem comum falar em TDD como uma série recursiva de ciclos “Red-Green-Refactor” que começariam por um nível mais alto (um teste de aceitação ou funcional, por exemplo) que gerariam novos pequenos ciclos até que o ciclo externo se tornasse verde. O que eu captei do teu artigo é que tu achou um certo nível onde esse ciclo não faz sentido pois o teu teste e teu código são redundantes (flashbacks de discussões sobre testes unitários de CSS).

    No entanto, tu também escreveu um artigo anterior onde tu ressalta a possibilidade de escrever testes automatizados para infraestrutura. Sendo que tu determinou que é possível escrever testes que fazem sentido (em contrapartida a esses testes unitários que tu constatou serem redundantes), tem algo que te impeça de fazer TDD com o tipo de teste que tu menciona no teu outro artigo? Se tu consegue escrever um teste que descreve alguma característica que teu sistema precisa ter antes de escrever o código que faz isso acontecer, tu estaria fazendo TDD, de certa forma. Teu teste não precisa usar um framework de teste nem ser unitário, só verificar algo que tu gostaria que acontecesse.

    Não estou tentando defender a ideia de que o TDD é necessário ou que funciona para infra pq também não sei a resposta, mas acho que teu exemplo prova que um certo nível de testes unitários para infra é redundante. Tem muitas outras abordagens que acho que valeria explorar antes de concluir que não funciona.

    • Obrigado pelo comentário Kao! Vou responder abaixo:

      “tem algo que te impeça de fazer TDD com o tipo de teste que tu menciona no teu outro artigo?”

      Fazer TDD com teste de integração ou de role é um pouco complicado o feedback é lento, já que para ele roda eu preciso criar a infraestrutura do zero 🙁

      “Tem muitas outras abordagens que acho que valeria explorar antes de concluir que não funciona.”

      Pode parecer que o texto conclui algo, mas não foi minha intenção. A ideia é puxar o debate mesmo, até porque o título fala “acho que não”. A ideia é discutir mesmo.

      • kcfelix

        Saquei. Pois é, imagino que o feedback lento seja um problema do teste no geral também, deve ser inconveniente tentar escrever um teste que leva 30 minutos pra saber se ele passa ou não, já que cada vez q ele falha eu teria q esperar 30 minutos de novo. No fim me pergunto se escrever o teste antes ou depois faria diferença no caso do feedback lento e se vale investigar se escrever o teste antes traz alguma vantagem ou desvantagem.

        • Acho que esse é o grande ponto, pois pra mim é bem claro a necessidade de testes, mas o problema mesmo está no fato de se fazer ele antes ou depois, uma vez que fazer antes pode impactar no andamento da escrita do código e depois podemos ocasionar de esquecer de fazer os testes, uma vez que já temos tudo implementado 🙁

  • Pingback: TDD para código de infraestrutura? Acho que não… – Parte 01 | grupo IO Multi Soluções Inteligentes()