Testes: O que são? Aonde vivem?

"Qualidade é grátis, mas só para aqueles que estão dispostos a pagar muito por ela" (DeMarco e Lister)

Se você é um(a) desenvolvedor(a) que está atualizado com as trends do mercado nos últimos meses, com certeza já deve ter ouvido algumas palavras como: TDD, Testes Unitários, Testes End-to-End, Integração contínua. Mas o que isso tudo significa para um desenvolvedor Javascript do século XXI?

Vamos explorar um pouco dos valores e vantagens de testes automatizados para esta plataforma de desenvolvimento em uma sequencia de três publicações sobre o tema!


Para que testar?

"O problema não é que o teste é o gargalo do pote. O problema é que você não sabe o que há dentro do pote! É isso que os testes solucionam." (Michael Bolton)

Bom, com certeza você, leitor, já ouviu ou até participou de um épico que tem mais ou menos essa cara:

  • Fizemos o desenvolvimento do produto
  • Tudo estava funcionando bem
  • Decidimos colocar uma nova feature
  • Testamos a feature, aparentemente tudo estava bem
  • Publicamos em produção
  • A aplicação toda caiu porque, em algum lugar, uma parte da mesma interagia com algo que esta feature mudou

Estes são temas muito comuns no meio de desenvolvimento de sistemas. Como trabalhamos com sistemas complexos e arquiteturas que, muitas vezes, não estão em poder de conhecimento de uma única pessoa, mas sim de um time, é fácil fazer com que uma adição ou um novo recurso quebre todo um pipeline de desenvolvimento. E é ai que entram os testes.

Um teste bem feito geralmente abrange todas as partes de uma aplicação, não só os chamados "caminhos felizes", aonde o resultado é o que o desenvolvedor espera que aconteça, por exemplo:

Veja que estamos com um teste válido, temos uma função de soma e estamos testando se ela está somando corretamente, mas e se o nosso desenvolvedor, ou usuário resolver fazer soma('batata', 'banana') ? O que vai acontecer?

Por isto a importância de testar não só a parte feliz dos nossos programas, mas também a parte triste, tentando abranger o máximo de caminhos que o nosso código pode tomar.

Um código imprevisível não é um bom código

O programador vidente

Sempre que escrevemos algum código, estamos, na verdade, informando o computador de uma sequencia de passos que devem ser realizados. A estes passos damos o nome da algoritmos, conjuntos de pequenas tarefas não ambíguas que devem ser executadas uma após a outra para que determinado resultado seja alcançado.

Por este motivo os erros em programação são chamados de exceções. Porque não estão previstas.

O problema que a maioria dos times enfrentam ao criar uma aplicação complexa (não necessariamente grande) é o famoso “Funciona na minha máquina”. Este é outro motivo pelo qual testes automatizados existem, eles são a garantia do time de que todo o código está se comportando da forma como ele deveria se comportar e não tem “vida própria”.

Uma das soluções que os testes visam trazer é acabar com aquela dúvida sobre se uma nova feature no sistema vai quebrar algo em um lugar completamente diferente (por isso é tão importante que os testes sejam atômicos).

Conheça sua criação

"Só sei que nada sei" (Sócrates)

É comum termos equipes divididas em times menores ou squads, de forma que cada pedaço do time e responsável por uma parte do produto. Isto é uma ótima prática de gerenciamento e desenvolvimento ágil, porém toda vantagem traz uma desvantagem.

Neste caso temos a vantagem de termos equipes muito mais gerenciáveis do que um time único absurdamente grande, mas também criamos o problema da fragmentação de responsabilidades, também chamado de “efeito linha de produção”, que basicamente significa que o time ficou tão especializado e tão granulado que os componentes do mesmo não tem conhecimento total do projeto, apenas da pequena parte na qual eles estão trabalhando.

Isto coloca a responsabilidade do conhecimento universal do sistema na mão de um gerente ou responsável, criando um único ponto de falha, o que não pode ocorrer em nenhuma hipótese.

A suite de testes é a única garantia do programador de que seu desenvolvimento não interferiu no desenvolvimento de outra pessoa do time. De forma que é possível continuar o desenvolvimento em equipe sem problemas.


Os conceitos

“Faça ou não faça, não existe tentativa” (Mestre Yoda)

A área de testes automatizados é tão grande e tem tantas informações que algumas empresas tem setores de qualidade próprios apenas para este tipo de trabalho. O tester geralmente é a pessoa que é contratada só para escrever e validar testes automatizados e de aceitação.

Essa fatia do desenvolvimento de software, como toda grande área, também é cheia de conceitos muito importantes não só para a criação de testes, mas também como estudo para melhoria contínua dos processos de desenvolvimento.

Testes unitários

Um teste unitário é, como o próprio nome já diz, o teste voltado para a unidade do sistema.

Ele visa testar componentes individuais (classes ou módulos) de forma que cada pedaço do sistema que é testado seja independente dos demais, ou seja, os testes unitários precisam ser self-contained e de forma que um teste não pode ser dependente de outro teste (isso inclui também estados globais) fazendo com que cada teste unitário execute por si só.

Outra característica importante do teste unitário é que ele não deve nem guardar e nem depender de outros estados de aplicação, ou seja, ele deve ser stateless (o que é mais ou menos uma obrigação para que tenhamos testes independentes). Quando digo “estados”, isso inclui qualquer tipo de propriedade que seja passada ou setada por outro teste, por exemplo, uma conexão de banco de dados compartilhada.

Todo teste unitário deve criar e destruir seus próprios recursos, sem utilizar os previamente existentes.

Resumindo:

  • Contido: Um teste unitário deve ser self-contained de forma que ele não pode depender de outros recursos ou testes;
  • Stateless: Um teste unitário não pode guardar nem gerar estados globais. Ele não pode saber de execuções prévias do seu próprio escopo ou de escopos externos;
  • Único: Cada teste unitário deve testar uma e somente uma unidade do sistema, um mesmo teste não deveria ter mais de uma responsabilidade;

Testes de integração

Testes de integração são o oposto dos testes unitários. Na verdade, está mais para uma extensão dos mesmos.

Em uma explicação simplória, um teste de integração nada mais é do que uma sequencia de testes unitários encadeados que compartilham um contexto comum, ou seja, ele integra uma ou mais partes do sistema em um único teste.

O objetivo do teste de integração e fazer aquilo que o teste unitário não faz: incluir o funcionamento global do sistema em sequência de forma que a saída de uma unidade passe para a próxima criando uma sequencia de execuções que utilizam o mesmo contexto. Em outras palavras, testa a integração entre as partes de um todo.

Testes End-to-End

Testes End-to-End também são conhecidos como testes de caixa preta (assim como os testes unitários são conhecidos como testes de caixa branca). Isto porque eles não estão interessados em saber o que acontece por dentro do capô do seu código, mas sim no que o usuário final está vendo.

Este tipo de teste é muito focado nas saídas de dados ao invés das lógicas internas, ou seja, para este teste é muito menos importante o tipo que uma função retorna, por exemplo, do que o tempo que ela leva para executar.

Geralmente testes deste tipo são confundidos com testes de integração. Isto não está errado, mas não podemos afirmar com clareza, a diferença é apenas uma palavra. Testes de integração visam testar se todos os módulos do sistema funcionam bem juntos, os testes integrados (ponto a ponto, End-to-End ou de sistema) são os testes que visam analisar o sistema como um todo.

Este tipo de teste geralmente é focado em visualizações, testando literalmente os elementos que dão exibidos na tela do usuário em cada caso.

Integração Contínua

Integração contínua é um termo teoricamente novo que basicamente remete a ideia de deploy continuo para os ambientes de produção baseado em ferramentas de controle de versão como o Git, SVN e Mercurial.

Em essência, o trabalho de um CI (Continuous Integration) é rodar os testes automatizados e realizar a publicação da aplicação para o ambiente de produção de forma automática a cada push para o repositório, mas somente se o comando de testes passar.

Essa é uma boa forma de automatizar os scripts que são repetitivos ou processos que devem ser feitos em todos os deploys. Claro, um CI é essencialmente um robô que pode ser programado para fazer qualquer tipo de trabalho repetitivo que for necessário, não só rodar testes, mas as vezes realizar a compilação e o envio de arquivos para o servidor por exemplo.

Modelo de uma integração contínua simples -> integração contínua complexa

Hoje existem diversas ferramentas do tipo Do It Yourself como o Jenkins e o Hudson, mas, em geral, o grande consenso da comunidade é utilizar ferramentas SaaS (Software as a Service) como o Travis, AppVeyor, CircleCI ou Wercker pelo fato de que eles já vem previamente configurados com as bases e o que é necessário e apenas criar um arquivo yaml de configuração customizada para a sua build.

Uma outra vantagem dos CI’s é poder rodar os testes em vários ambientes e sistemas operacionais diferentes de forma que a cobertura contra erros fica muito mais abrangente.

TDD

É a sigla para Test Driven Development, ou seja, o desenvolvimento orientado a testes.

Neste paradigma de desenvolvimento, tudo gira em torno dos testes automatizados. Uma das grandes diferenças entre este paradigma e o paradigma “comum” é que, no TDD, os testes são feitos "primeiro" e o código propriamente dito sucede o mesmo.

Ciclo RGR

A palavra "primeiro" está entre aspas por um motivo: Não são necessariamente os testes que vem primeiro de tudo. O TDD segue um ciclo chamado de RGR (Red Green Refactor), isso significa que o primeiro teste tem o objetivo de falhar, ou seja, o teste deve retornar uma falha (por isso o red) quando for executado pois provavelmente não vai haver nenhum código escrito (ou algo simples), após isso o programador deve reescrever o código com o objetivo de passar o teste (temos o nosso green) e, por fim, tendo um teste que passa e outro que falha, agora só nos resta refatorar nosso código para limpá-lo e reorganizá-lo melhor, pois sempre há espaço para melhoria (daí o refactor) e recomeçar o ciclo.

Isto é bom por alguns motivos:

  • Tendo os testes de antemão, o código deve seguir um modelo para que se encaixe neles, não o contrário
  • O desenvolvimento já orientado aos testes que deverão ser feitos faz com que o desenvolvedor tenha uma noção muito maior do seu sistema como um todo
  • As partes do sistema deverão ser bem documentadas e definidas antes da escrita dos testes, para que estes possam exprimir todo o comportamento da mesma

Em geral o TDD faz com que sua aplicação tenha de seguir uma receita para que, no final, ela possa ser testada de forma aceitável. Este paradigma força a equipe a pensar de forma inversa, primeiro focando no resultado final e depois no desenvolvimento, evitando assim as chamadas “gambiarras”.


Conclusão

Este artigo teve a intenção de ser uma porta de entrada para o mundo dos testes automatizados. Nos próximos dois artigos vou cobrir os conceitos de forma mais aprofundada, explicando os conceitos de forma mais fácil e com exemplos de uso.

Até lá!