Rust | Macros procedurais e testes automatizados

Escrevendo testes integrados para meu projeto Moy Sekret, senti falta de um recurso nativo do Rust para fazer setup & teardown, antes e depois dos casos de teste em si, para checar e limpar alguns efeitos colaterais dele em disco, por se tratar de um programa CLI que lida com criptografia de arquivo.

Sendo Rust uma linguagem compilada para binário nativo, sem um runtime, como Java ou C#, a solução mais óbvia para resolver isto com meta-programação foi criar uma macro.

Macro rules!

Na minha primeira interação para resolver meu problema, então, criei uma macro declarativa simples para injetar um bloco de código before e after imediatamente antes e logo após as funções de testes.

Fonte: https://github.com/leandrosilva/moy-sekret/blob/master/tests/common.rs

O que a macro setup_run_test faz é criar uma outra macro, a run_test, que é a real responsável por rodar o caso de testes, sanduichado pelos blocos before_test e after_test definidos através da setup_run_test.

Depois, para fazer uso destas macros é bem simples.

Fonte: https://github.com/leandrosilva/moy-sekret/blob/a165a570c36af0eae6e020608511a43240e2fda1/tests/encrypt_test.rs

Uma vez que a macro tenha sido exportada em sua definição, quando o módulo em que ela foi definida é importado, ela torna-se também disponível para uso no contexto atual.

Programação por convenção

A despeito destas macros terem dado um bom adianto, para eu não ter que escrever do_something(); e do_something_else(); em todos os meus casos de teste (que por enquanto nem são tantos, para ser franco), isto ainda me pareceu muito trabalho ter que fazer setup dos blocos before e after e depois usar a macro run_test! em cada caso de teste.

Por que não seguir uma convenção em vez de fazer uma configuração? You bet!

Macros procedurais

Rust tem um recurso [já não tão novo assim] chamado Procedural Macros, que permite que você manipule um trecho de código fonte (AST) em tempo de compilação e produza um código diferente para ser compilado.

Há três tipos de macros procedurais:

1) Derive macros (#[derive(AwesomeDerive]) – tipo de macro já bem estável, desde a versão 1.15 de Rust, e de uso razoavelmente comum.

Fonte: https://github.com/leandrosilva/moy-sekret/blob/master/src/lib.rs

2) Function-like macros (pretty_macro!()) – este tipo de macro está estável desde a edição 2018 de Rust. É um tipo muito interessante, parecido com macro_rules!, porém bem mais flexível, já que você tem bastante liberdade em relação aos parâmetros que podem ser aceitos.

Pense em uma função que execute SQL, por exemplo.

Todo este código SQL acaba sendo encapsulado em um TokenStream, que é passado para uma função sql, que finalmente pode parseá-lo e blá, blá, blá.

3) Attribute macros (#[GorgeousAttribute])- este tipo também tornou-se estável em Rust 2018. É muito parecido com as anotações que temos em Java ou C#, porém estritamente em tempo de compilação e permitem fazer transformações no código.

Este foi o tipo de macro que ajudou a resolver minha preguiça de digitar meia dúzia de linhas de código.

Entra o Testaun

O projeto Testaun é o crate que eu criei para conter macros que me ajudem a reduzir código nos meus testes. Hoje, tudo que ele tem é uma macro. Mas por hora, é tudo que eu preciso.

Fonte: https://github.com/leandrosilva/testaun/blob/master/src/lib.rs

O que esta macro procedural faz é extrair o bloco de código de uma função de testes e sanduichar ele com chamadas às funções testaun_before e testaun_after. Na prática, o mesmo que a macro run_test! que mostrei anteriormente.

Lembra? Em lugar de fazer o setup de dois blocos para serem executados antes e depois, usamos convention over configuration e esperamos que estas duas funções tenham sido definidas. Caso contrário, pau! O compilador vai chiar.

Okay. E como é que se usa isto depois?

Tendo adicionado este crate ao projeto, basta criar as funções que rodarão antes e depois do caso de teste, anotar a função de teste e é isto.

Fonte: https://github.com/leandrosilva/testaun/blob/master/tests/tasting_test.rs

Como o código acima exemplifica, se um caso de teste não precisa de before & after, basta não anotar com #[testaun_case].

Uma coisa que percebi foi que o crate serial_test (que também manipula AST) e o testaun não se dão bem juntos. Testaun boicota o serial_test. Vou estudar a coexistência deles depois.

Não se reprima. Digo, não se REPITA!

Se você for como eu, um programador preguiçoso, a DRY kinda guy, você pode usar macros para economizar umas linhas de código repetitivo, reduzir boilerplate, padronizar o código do seu projeto e, no final das contas, torná-lo menos suscetível a falhas.

Menos código repetido, menos bugs.

Aliás, menos código total, menos bugs ainda.

Um pouco de TDD com Eunit

No final do ano passado estava bem empolgado com a implementação Erlang do Restfulie e, algum tempo depois de ter começado o projeto, resolvi deixar de ser cowboy e escrever alguns testes. De lá pra cá, vez ou outra, dei uma melhorada nos testes, primeiro pra ficar mais útil, depois pra deixar mais fluente. Ainda não está lá essas coisas, mas já está, no mínimo, interessante. Se você quiser, pode conferir no meu GitHub.

Bem, no final das contas, isso acabou me motivando a escrever esse post, como um tutorial básico de TDD (talvez alguns queriam chamar de BDD, não tem problema, podem chamar como assim) com Eunit, a biblioteca de testes padrão que vem com qualquer instalação de Erlang.

A idéia desse tutorial é seguir numa linha um pouco diferente da documentação do Eunit e dos exemplos contidos no próprio projeto, escrevendo testes mais fluentes e interessantes de ler.

Mão na massa

Passo 1: criar a estrutura do projeto

O nome do nosso projeto será making_tdd_with_eunit (bem criativo!). Vamos criar então a seguinte estrutura de diretórios, que é frequentemente encontrada em projetos Erlang:

- making_tdd_with_eunit
  |
  +-- ebin
  +-- src
  +-- test

OK, apesar dos diretórios terem nomes auto explicativos, não custa nada reforçar, né?

ebin, contém os binários (.beam) resultantes da compilação dos fontes;
src, contém os fontes (.erl) dos módulos;
test, contém os fontes (.erl) dos testes.

Passo 2: escrever o primeiro teste que inevitavelmente falhará

Qual é o fluxo de trabalho com TDD mesmo, hein? Escrever o código a ser testado, rodar e, se funcionar, escrever o tal teste que passa de primeira, não é isso? Não, não é. Isso não é TDD.

Pois muito bem, vamos fazer então como realmente tem que ser feito. No diretório test, crie o seguinte teste (mobile_test.erl):

-module(mobile_test).

-include_lib("eunit/include/eunit.hrl").

describe_client_test_() ->
  {"Mobile",
    [fun should_have_a_number/0]}.

should_have_a_number() ->
  ?assertMatch({number, _}, mobile:number()).

Feito isso, compile e rode o teste com os seguintes comandos:

$ erlc -o ebin/ test/mobile_test.erl
$ erl -pa ebin/ -noshell -run mobile_test test -run init stop

Qual foi o resultado? O teste falhou, certo? Pois é, como manda o figurino!

“Escreva um teste, rode todos os testes e veja-o falhar; escreva o código mais simples possível para fazê-lo passar, mesmo que num primeiro momento não seja o mais bonito e eficiente; rode todos os testes e veja-os passar; depois, refatore para remover duplicações.” [1]

Passo 3: fazer o teste passar

Agora vamos fazer esse teste passar. No diretório src, crie o seguinte módulo:

-module(mobile).

-export([number/0]).

number() ->
  {number, "1212-1212"}.

Como fizemos anteriormente — agora acrescentando o módulo mobile à compilação —, compile e rode o teste com os seguintes comandos:

$ erlc -o ebin/ src/mobile.erl test/mobile_test.erl
$ erl -pa ebin/ -noshell -run mobile_test test -run init stop

Resultado? “Test passed.”

Passo 4: escrever mais testes

Só para não prolongar muito — este que deveria ser um pequeno tutorial — vamos para o ponto em que magicamente passamos a ter testes para as quatro funções (ultra dummies) contidas no módulo mobile, tudo bem?

-module(mobile_test).

-include_lib("eunit/include/eunit.hrl").

describe_client_test_() ->
  {"Mobile",
    [fun should_have_a_number/0,
     fun should_have_a_area_code/0,
     fun should_have_a_company/0,
     fun should_have_a_owner/0]}.

should_have_a_fixed_number() ->
  ?assertMatch({number, "1212-1212"}, mobile:number()).

should_have_a_fixed_area_code() ->
  ?assertMatch({area_code, "11"}, mobile:area_code()).

should_have_a_fixed_company() ->
  ?assertMatch({company, "DEAD"}, mobile:company()).

should_have_a_fixed_owner() ->
  ?assertMatch({owner, "Little Jose"}, mobile:owner()).
-module(mobile).

-export([number/0, area_code/0, company/0, owner/0]).

number() ->
  {number, "1212-1212"}.

area_code() ->
  {area_code, "11"}.

company() ->
  {company, "DEAD"}.

owner() ->
  {owner, "Little Jose"}.

OK, repetindo o passo anterior de compilação e execução dos testes, temos quatro testes passando, certo? Então é hora de um pouco de refatoração…

Passo 5: refatorar os testes para ficarem mais fluentes

Nesse ponto, vamos adicionar mais fluência ao nosso teste e melhorar a sua comunicação, inclusive, descrevendo um cenário “bem interessante”.

-module(mobile_test).

-include_lib("eunit/include/eunit.hrl").

describe_client_test_() ->
  {"Mobile",
    {"when is a dummy",
      [
        {"should have a fixed number",
          fun should_have_a_fixed_number/0},
        {"should have a fixed area code",
          fun should_have_a_fixed_area_code/0},
        {"should have a fixed company",
          fun should_have_a_fixed_company/0},
        {"should have a fixed owner",
          fun should_have_a_fixed_owner/0}
      ]}}.

should_have_a_fixed_number() ->
  ?assertMatch({number, "1212-1212"}, mobile:number()).

should_have_a_fixed_area_code() ->
  ?assertMatch({area_code, "11"}, mobile:area_code()).

should_have_a_fixed_company() ->
  ?assertMatch({company, "DEAD"}, mobile:company()).

should_have_a_fixed_owner() ->
  ?assertMatch({owner, "Little Jose"}, mobile:owner()).

Compilando e rodando os testes: “All 4 tests passed.”

Próximo passo: escrever mais testes e refatorar o módulo mobile

Até aqui, temos um módulo mobile totalmente dummy e um teste que usa apenas a macro ?assertMatch. Ainda há muito que pode ser feito, desde adicionar algum comportamento útil ao módulo mobile, até melhorar os testes, fazer diferentes asserções, adicionar befor_all e after_all (como nos testes do Restfulierl, por exemplo) para estado inicial e final, e por aí vai.

Bem, esse passo deixo por sua conta. É um bom exercício pra você praticar mais Erlang e Eunit.

Espero que tenha gostado e até mais!

[1] Beck, K. Test-Driven Development: By Example. Addison-Wesley Professional, 2002.

Refatorar é preciso!

[Novo endereço: leandrosilva.com.br.]

Definitivamente, refatorar é preciso!

Ninguém consegue escrever o melhor código do mundo na primeira vez que o escreve. Aliás, arrisco dizer que isto não é verdade apenas com software, mas com uma infinidade de coisas que fazemos ao longo da vida.

Desde que me entendo por gente garoto, sempre tive o hábito de refatorar. Nos meus trabalhos escolares, composições musicais, estudos bíblicos. Dia desses mesmo tive que escrever uma carta para ser lida publicamente [a detendos e familiares] em um presídio de São Paulo. Que responsabilidade! Escrevi e refatorei várias vezes numa mesma madrugada até a tal carta ficar muito boa.

No meu trabalho não é diferente. Escrevo e re-escrevo códigos sempre. Não por terem sido mal escritos por mim ou por algum colega, mas porque acredito que eles sempre podem ser aprimorados.

Refatoração como disciplina da Engenharia de software:

Martin Fowler, em seu site oficial sobre refatoração, diz que refatorar é alterar a estrutura interna de um código sem alterar o seu comportamento externo. É fazer pequenas modificações de cada vez, que somadas, resultem em modificações significativas, sem contudo, afetar o funcionamento do código.

De forma bem prática, no meu dia a dia, costumo enumerar as tarefas da refatoração em: Revisar, remolelar e refinar. Três ‘R‘s indispensáveis na vida de todo bom programador, porque:

1. Todo código precisa ser revisado;
2. Todo código é forte candidato a ser remodelado, pelo menos uma vez;
3. Todo código deve ser refinado sempre.

Quando aplicada corretamente, a refatoração trás excelentes ganhos, tais como:

1. Códigos mais legíveis;
2. Códigos mais performáticos;
3. Códigos mais extensíveis;
4. E sobretudo, código mais simples!

Este e o sentimento da refatoração. Ganhos hoje; e mais ainda, amanhã.

Agora, sabe qual é a triste realidade? Poucos gestores de projetos entendem e encorajam esta prática. Uma pena, porque o projetos só tem a ganhar. Quantas vezes você já não entrou em um projeto e perdeu horas e horas pra entender o código macarrão que alguém que nem está mais na empresa fez? Quantas vezes mais isso ainda não vai acontecer neste mesmo projeto? Ah, se estas horas tivessem sido empregas em refatoração…

Mas, sabe de uma outra coisa? A culpa não é só dos gestores de projetos. Desenvolvedores de software também precisam aprender a vender suas idéias, soluções, e mostrar resultados reais da aplicação de determinadas técnicas. Afinal de contas, cientistas renomados como Martin Fowler costumam ter razão naquilo que dizem.

Bom, ainda bem que aqui na equipe de arquitetura da CVC Turismo não é assim. Aqui refatorar é visto com muito bons olhos. Bons olhos sobre quem faz a refatoração, dado o seu pró-ativismo; e bons olhos sobre o próprio patrimônio de software da empresa que é valorizado a cada dia.

Se seu gestor não quer nem ouvir falar em refatoração, eu tenho uma sugestão pra você: Faça algumas refatorações por conta própria, sem que estas afetem o andamento do seu projeto, é claro, e depois apresente a ele um relatório de ganhos, conforme citados anteriormente.

Não se esqueça: Frutos falam mais que palavras.

Depois poste aqui o resultado…