Artigo no estilo: Curso

Por que eu devo ler este artigo:Nenhuma categoria do ciclo de vida de software está em tanta evidência quanto os testes unitários.

Ao mesmo passo, o advento das tecnologias front-end, aliado aos processos e boas práticas que cada vez mais se fazem necessários de serem aplicados a esse meio, trazem à tona a real necessidade de unir ambos universos: testes de unidade ao desenvolvimento client side.

Este artigo explicita isso trazendo a primeira de duas partes sobre o desenvolvimento de testes unitários usando JavaScript. Veremos todos os conceitos, práticas e metodologias que norteiam o que há de mais recente no assunto, assim como modelos básicos de como eles se aplicam ao uso do JavaScript e seus frameworks.

No advento das tecnologias front-end e da utilização de novos padrões de desenvolvimento, como a HTML5, novas fórmulas e conceitos foram criados de forma a suprir a cada vez maior necessidade de atender a todos os diversos cenários de software.

O uso de testes unitários para verificar se um módulo ou unidade do código está funcionando corretamente como esperado já não constitui nenhuma novidade. Afinal, tal conceito foi outrora introduzido na comunidade de desenvolvedores por Kent Beck, através de linguagens mais antigas como Smalltalk, C, C++ e até mesmo no Java.

Porém, se analisarmos bem o termo é sempre extremamente associado às linguagens que se constituem “linguagens server side”, isto é, executam apenas do lado do servidor, muitas com processos bem definidos de compilação, interpretação, plataforma integrada ou multiplataforma, etc.

Até mesmo uma rápida busca na web pelo termo irá remeter em inúmeras bibliografias, blogs e sites sobre o assunto com foco voltado para esse tipo de linguagem.

É interessante notar que até mesmo o conceito foi desenvolvido e evoluído sobre esse tipo de linguagem. Diante disso, este artigo visa construir uma ideia adaptada dos testes unitários para o universo front-end, especificamente focado no uso e construção dos exemplos em cima da linguagem de script mais famosa do mercado: JavaScript.

Logo, aqui abordaremos os tópicos mais famosos dos testes, desde os conceitos, teorias e boas práticas, até a escolha das ferramentas adequadas para tal.

Testes unitários

Antes de entender quais divisões ou aplicações de testes diversas existem no universo de programação, é importante antes entender o que é, de fato, um teste unitário. O próprio nome já consegue nos dizer muito acerca desse procedimento: um teste ou mais testes que verificam unidades de composição de, no nosso caso, sistemas diversificados.

Se procurarmos mais formalmente, veremos algumas definições mais elaboradas: “Em programação de computadores, teste unitário ou teste de unidade constitui um método de teste de software pelo qual unidades individuais de código fonte, configuradas com um ou mais módulos de programas de computador juntos com dados de controle associados são testados para determinar se eles estão aptos para o uso final.”

Se pensarmos programaticamente uma unidade de código, e consequentemente de software, deve constituir o menor pedaço testável de uma aplicação. Em linguagens orientadas a objetos isso pode representar um método, ou até mesmo um bloco menor de código fonte.

Porém, quando partimos para o paralelo das linguagens procedurais essa mesma unidade pode significar o módulo inteiro programado, mesmo sendo comuns as divisões por módulos menores ou funções e procedures individuais.

Em resumo, os testes unitários nos permitem adentrar métodos e classes para verificar a consistência da implementação dos mesmos. E isso significa que uma bateria de testes bem elaborada traz consigo uma carga de segurança alta em relação ao código implementado, uma vez que temos agora uma camada que seguramente avalia todas as unidades de código quando da refatoração ou alteração dos mesmos.

Em outras palavras, é possível introduzir novas partes ao código existente (um método, por exemplo) e poder verificar sua consistência de igual forma.

Alguns desenvolvedores, no entanto, consideram que a prática de escrever “código para testar código” constitui um desperdício de tempo, tempo este que poderia ser usada para focar nas próximas iterações de desenvolvimento, bugs, ou quaisquer atividades associadas.

Porém, quando se trata do desenvolvimento de aplicações, com várias pessoas trabalhando na mesma equipe (muitas delas alterando os mesmos pontos de código), com várias iterações, versões do código e dependendo da complexidade do sistema, os testes unitários vêm para, na verdade, salvar tempo.

Basta considerar o mapeamento de erros que te deixaria rápida e seguramente atualizar o código fonte, por exemplo.

A ideia de testar partes do código como um todo não é recente, data de meados de 1970, quando Kent Beck, um dos responsáveis também pela metodologia de desenvolvimento ágil XP (Extreme Programming), idealizou o conceito baseado na necessidade de mais produtividade quando da aplicação de mudanças ao software durante o seu ciclo de vida.

A consolidação, porém, só veio quando da criação dos frameworks xUnit, que permitiam a verificação de partes separadas do código-fonte com os mesmos fins descritos até então.

Como o termo até então remetia apenas à linguagem Smalltalk, e com a popularização crescente do Java, tornou-se necessário então a criação de um framework para a linguagem, daí nasceu o famoso JUnit. Apesar de ser uma linguagem server side (no conceito de aplicações cliente-servidor), o Java e o JUnit tiveram papel de extrema importância para a popularização e aceitação dos testes unitários por parte dos desenvolvedores de uma forma geral.

A partir dessa linguagem, foi que tivemos uma comunidade ativa e aberta à discussão do assunto, além de contribuir para a evolução do conceito e respectivas implementações.

Design do teste

Existem uma série de propriedades a serem consideradas quando da criação e utilização de testes unitários:

● Ele deve ser automatizado e permitir repetição;

● Ele deve ser fácil de implementar;

● Uma vez escrito, ele deve ser mantido para usos futuros;

● Qualquer um deve estar apto a executá-lo;

● Ele deve executar através do clique de um botão;

● Ele deve executar rapidamente.

Com um teste unitário, o sistema sobre o teste (SUT) deve ser pequeno e apenas relevante para os desenvolvedores que trabalham proximamente ao código. Um teste unitário deve considerar operações do tipo: código lógico, código que contém muitos branches, cálculos or algo que de alguma forma requeira algum tipo de decisão. Simples métodos getters e setters são exemplos de código não lógico.

Quando um software é desenvolvido sob a perspectiva do teste unitário, a combinação de escrever um teste unitário para especificar a interface mais o refactoring das atividades depois que o teste passa constituem o design formal do teste. Cada teste unitário pode ser visto como um elemento de design especificando classes, métodos e comportamentos observáveis. O teste exemplificado na Listagem 1 ilustra bem essa ideia.

Listagem 1. Exemplo de teste unitário em Java

  public class AdicaoTeste {
   
      // pode somar números positivos 1 e 1?
      public void testeSomaNumeroPositivoUmEUm() {
          Soma soma = new SomaImpl();
          assert(soma.add(1, 1) == 2);
      }
   
      // pode somar os números positivos 1 e 2?
      public void testeSomaNumeroPositivoUmEDois() {
          Soma soma = new SomaImpl();
          assert(soma.add(1, 2) == 3);
      }
   
      // pode somar os números positivos 2 e 2?
      public void testeSomaNumeroPositivoDoisEDois() {
          Soma soma = new SomaImpl();
          assert(soma.add(2, 2) == 4);
      }
   
      // O zero é neutro?
      public void testeSomaZeroNeutro() {
          Soma soma = new SomaImpl();
          assert(soma.add(0, 0) == 0);
      }
   
      // pode somar os números negativos -1 e -2?
      public void testeSomaNumerosNegativos() {
          Soma soma = new SomaImpl();
          assert(soma.add(-1, -2) == -3);
      }
   
      // pode somar um número positivo e um negativo?
      public void testeSomaPositivoENegativo() {
          Soma soma = new SomaImpl();
          assert(soma.add(-1, 1) == 0);
      }
   
      // E quanto aos números grandes?
      public void testeSomaNumerosGrandes() {
          Soma soma = new SomaImpl();
          assert(soma.add(1234, 988) == 2222);
      }
  }
   
  interface Soma {
      int add(int a, int b);
  }
  class SomaImpl implements Soma {
      int add(int a, int b) {
          return a + b;
      }
  }

Na mesma listagem é possível observar um conjunto de casos de teste que especificam um número de elementos da implementação. A interface Soma é responsável por manter o contrato de assinaturas de acordo com a sua classe de implementação, SomaImpl. A implementação verifica (assert) o comportamento de diferentes chamadas (situações, casos) ao método add().

Neste caso, os testes unitários, tendo sido escritos antes, atuam como um documento de design especificando a forma e comportamento de uma determinada solução, mas não detalhes de implementação, que serão deixados para o desenvolvedor.

Seguindo o paradigma “Faça a mais simples coisa que possa possivelmente funcionar”, a solução mais fácil que irá fazer com que o teste passe está na classe SomaImpl e interface Soma.

Ao contrário de outros métodos de design, usar testes unitários como uma especificação de design tem uma vantagem significativa: o documento de design pode ser usado para verificar se a implementação “adere” ao design. Com esse método de design os testes nunca passarão se os desenvolvedores não implementarem a solução de acordo com o design.

Tipos de teste

É extremamente importante saber a diferença entre os tipos de teste existentes, bem como suas nomenclaturas e conceitos. Existem três divisões principais de testes: o teste de unidade (que tratamos aqui), de integração e de sistema. Vejamos as diferenças do teste de unidade para com as outras:

● O teste de integração, como o próprio nome já diz, é o teste responsável por averiguar a integração entre duas partes do seu sistema. Um dos exemplos mais clássicos desse tipo de teste é representado pelo padrão de projetos DAO, que faz acesso à base de dados para persistir as informações manipuladas pela linguagem de programação.

Ao desenvolver testes para uma classe desse tipo você automaticamente estará criando um meio de verificar se a "integração" entre ambos, código e banco de dados, existe e está conforme.

Existem vários outros tipos de integrações que podem ser verificadas via testes de integração, tais como código que se comunica com web services, que efetuam operações de escrita e leitura sobre arquivos, que verificam a comunicação assíncrona da implementação de filas e JMS, ou até mesmo que verificam o funcionamento de mensagens enviadas via sockets, por protocolos HTTP, etc.

● O teste de sistema é uma expressão usada para designar se o funcionamento do sistema como um todo está procedendo. O foco, portanto, deixar de ser a unidade (teste unitário) assim como a integração (teste de integração) e passa agora a focar na junção de todas as pequenas partes do sistema constituindo o conjunto completo de tarefas a serem executadas pelo sistema. Também chamado de "teste de caixa preta", esse tipo de teste lida com a averiguação de tudo, desde o banco de dados, métodos e classes até as integrações.

Para algumas vertentes, os testes de aceitação, que ficaram famosos com o advento do desenvolvimento ágil e são caracterizados pela aceitação ou não do time ágil, são no final testes de sistema.

Diante dessa situação, muitos desenvolvedores se perguntam qual tipo de teste escolher e/ou como associar cada nível de teste a uma situação em específico no ciclo de desenvolvimento do mundo real. Na verdade, não existe bala de prata, é preciso ter o discernimento de associar cada situação a um tipo de teste específico.

Por exemplo, as classes responsáveis por lidar com o negócio da aplicação geralmente podem ser testadas em unidade, de uma maneira isolada, enquanto classes que se comunicam com web services necessitam de um teste de integração aplicado, dada a necessidade do outro ambiente para tal teste. Além disso, vale frisar a importância do teste bem feito, o que quase sempre significará ter testes para ambas as situações onde o teste funciona e quebra.

Tipos de processos

Além de analisar os tipos de testes que existem, é importante também saber a diferença entre estes e os diferentes tipos de processos de teste que existem e que envolvem todo esse ciclo. Essas famosas “práticas de desenvolvimento” se dividem em três nomes tão famosos quanto: TDD - Test-Driven Development, BDD - Behavior-driven Design e DDD - Domain-driven Design (Figura 1).

Figura 1. Três tipos de processos de desenvolvimento usando testes

TDD

O TDD, Test-Driven Development (ou Des ...

Quer ler esse conteúdo completo? Seja um assinante e descubra as vantagens.
  • 473 Cursos
  • 10K Artigos
  • 100 DevCasts
  • 30 Projetos
  • 80 Guias
Tenha acesso completo