Por que eu devo ler este artigo: 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.

Nenhuma categoria do ciclo de vida de software está em tanta evidência quanto os testes unitários.


Guia do artigo:

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.

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).

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

TDD

O TDD, Test-Driven Development (ou Desenvolvimento Orientado a Testes) é uma metodologia famosa que cresceu junto com o desenvolvimento ágil e que foca no lema "testar primeiro, desenvolver depois". Em outras palavras, é uma técnica que visa iterações curtas e que propõe que um dado desenvolvedor implemente primeiramente o caso de teste específico para uma situação de construção de funcionalidade, correção de bug, etc., e em seguida, e somente em seguida, crie o código fonte que atenderá e será validado pelo caso de teste feito anteriormente.

Ele ganhou mais atenção nos anos posteriores como diferentes metodologias junto ao processo de software que vinha surgindo. Dissecando o nome, desenvolvimento sugere um processo de pleno direito com a análise, projeto lógico e físico, implementação, teste, análise, integração e implantação, e orientado a testes implica o quão concreto os testes automatizados devem conduzir o processo de desenvolvimento. O TDD também é comumente chamado de programação test-first.

Quando se houve falar sobre essa terminologia, principalmente associada a uma explicação do seu conceito, a ideia é aparentemente simples. Apenas dando uma breve olhada na própria palavra é possível ver que TDD remete a ter testes que dirigem o desenvolvimento do software.

Se adentramos um pouco além na definição desse conceito, nos deparamos com cinco diferentes estágios:

  1. Antes de qualquer coisa, o desenvolvedor escreve alguns testes;
  2. O desenvolvedor então executa alguns destes testes e (obviamente) eles irão falhar porque nenhuma destas features estão de fato implementadas;
  3. Em seguida, o desenvolvedor na verdade implementa todos os testes feitos antes, agora em código fonte da linguagem escolhida;
  4. Se o desenvolvedor escreve o seu código bem, então no próximo estágio ele irá ver seus testes serem executados com sucesso;
  5. O desenvolvedor pode agora refatorar seu código, adicionar comentários, limpar o projeto de uma forma geral, como ele deseja porque o desenvolvedor sabe que se o código novo quebrar em algum local, então os testes irão alertá-lo sobre a falha.

O ciclo pode somente continuar se o desenvolvedor tem mais features a adicionar. Veja na Figura 2 a representação desse fluxo.

Fluxo de execução do TDD
Figura 2. Fluxo de execução do TDD.

Vejamos um exemplo então de como um desenvolvedor deveria fazer isso. Digamos que um desenvolvedor quer escrever uma função que faz algo bem simples, como calcular um fatorial (obviamente um exemplo bem simples, mas isso será o suficiente para descrever como o comportamento TDD deva ser). A abordagem normal para TDD indica usar a função e então o assert para que o resultado satisfaça um determinado valor.

Os testes deverão ser parecidos com o código demonstrado na Listagem 2, totalmente desenvolvido sobre o framework JavaScript Mocha.

Listagem 2. Exemplo de TDD com JavaScript Mocha.

  var assert = require("assert"), fatorial = require("../index");
   
  suite("Teste", function (){
      setup(function (){
          // Cria quaisquer objetos que possam ser necessários
      });
   
      suite("#fatorial()", function (){
          test("igual a 1 para configurações de tamanho zero", function (){
              assert.equal(1, fatorial(0));
          });
   
          test("igual a 1 para configurações de tamanho um", function (){
              assert.equal(1, fatorial(1));
          });
   
          test("igual a 2 para configurações de tamanho dois", function (){
              assert.equal(2, fatorial(2));
          });
   
          test("igual a 6 para configurações de tamanho três", function (){
              assert.equal(6, fatorial(3));
          });
      });
  });

Os testes com certeza falharão porque a função ainda não foi escrita, mas é justamente essa a intenção: verificar como o comportamento do código deveria funcionar, mesmo não tendo um código ainda para testes. Agora que já entendemos a abordagem nua e crua, criemos o código da função que irá satisfazer os testes. Ele deverá se parecer com o código representado pela Listagem 3.

Listagem 3. Resultado de comando de execução do Java version.

  module.exports = function (val) {
      if (val < 0) {
            return NaN;
         }
      if (val === 0) {
            return 1;
         }
   
      return val * fatorial(val - 1);
  };

Agora se executarmos os testes, poderemos ver que todos eles passarão com sucesso. Isso é como o TDD funciona.

Ao utilizar esse tipo de processo, alguns benefícios se tornam claros logo já, como o baixo acoplamento com uma série de testes que comprovam o seu comportamento. Um dos resultados mais visíveis do TDD é que os testes desenvolvidos sobre o mesmo fornecem uma espécie de documentação informal do sistema, conforme falamos anteriormente, ditando não só o que o sistema pode fazer mas também o que não deve fazer (e é aí onde entram os testes falhos).

Além disso, pelo simples fato de tudo isso estar integrado ao software, podemos dizer que o produto teste nunca ficará ultrapassado, o que difere das atividades de escrita de documentação e/ou comentar código.

Alguns outros prós se fazem mais sutis, porém importantes, no ciclo de vida do software bem como na adesão de produtividade ao projeto, tais como a diminuição do tempo usado para depurar código, o que implicará no lucro de tempo e, consequentemente, dinheiro.

Tudo isso é possível pelo simples fato de o TDD informar a quem o usa sempre que um erro ocorrer no sistema, diminuindo o tempo de depuração e deixando-a menos custosa.

BDD

O BDD foi primeiramente introduzido por Dan North no seu artigo "Instrução ao BDD" e é uma metodologia que evoluiu das práticas de TDD. BDD é uma técnica de design sobre user stories, que deve ser escrita em uma linguagem inteligível por não-programadores.

Isso possibilita que todos os envolvidos no projeto, executivos, testers, usuários e outros funcionários a se tornarem mais ativos no desenvolvimento. As user stories são centradas na seguinte sintaxe:

Título (uma linha descrevendo a história)

Narrativa:

Como um(a) [regra]

Eu quero [funcionalidade]

Que então [beneficia]

Critério de aceitação: (apresentado como cenários)

Cenário 1: Título

Dado um [contexto] e [mais alguns contextos]...

Quando [evento]

Então [saída] e [outras saídas]...

Como ocorre com o TDD, BDD é uma técnica de design outside-in. No BDD as histórias são escritas primeiro, então verificadas e priorizadas pelos usuários e envolvidos não-técnicos. O programador então cria o código para atender às histórias descritas. Em BDD, existem três princípios-base:

  • Negócio e tecnologia devem referenciar o mesmo sistema da mesma forma;
  • Qualquer sistema deve ter um valor identificado e variável para o negócio;
  • Análises do tipo up-front, design e planejamento têm um retorno cada vez menor.

O primeiro princípio base está solidificado sobre a user story, escrita conforme mostramos acima, de uma maneira não-técnica. Essa noção de linguagem natural segue muitos frameworks BDD, mesmo em um nível de código. O segundo princípio base de adicionar valor de negócio pode ser visto na história como a parte "Que então [beneficia]".

O terceiro princípio tem o mesmo significado do processo TDD. Um design upfront é feito da forma mais pequena o possível, mesmo que o design esteja indo por além do teste e refactoring.

Juntos os princípios ajudam os desenvolvedores a mitigar os riscos de criar recursos extra ou de criar funcionalidades de forma errada. Até hoje, o BDD não tem recebido tanta atenção na comunidade de pesquisas, mas isso provavelmente tenha relação com a sua idade e sua origem de método de pesquisa, o TDD, que ainda é alvo de muito contradição.

Ok, podemos dizer que entendemos o que é BDD. Mas, é ainda brota algumas clássicas e famosas confusões. Alguns dizem que o BDD é similar ao TDD (fato que justifica ainda mais o porquê de uma ter surgido a partir da outra), alguns outros irão dizer que é o mesmo que TDD porém com alguns guidelines melhores, ou ainda que são abordagens totalmente diferentes para o desenvolvimento.

Independente da definição, isso não importa muito. A principal coisa a se saber é que o "BDD é feito para eliminar problemas que o TDD venha a causar".

Em contraste ao TDD, o BDD consiste em escrever comportamento e especificação que então dirija o desenvolvimento do nosso software. Comportamento e especificação devem ser vistos como similares para testes mas a diferença é deveras importante.

Vejamos, então, novamente o problema descrito na seção anterior, quando falávamos sobre TDD, sobre escrever uma função para calcular o valor fatorial de um número (Listagem 4).

Listagem 4. Resultado de comando de execução do Java version.

  var assert = require("assert"), fatorial = require("../index");
   
  describe("Teste", function (){
      before(function(){
          // Coisas a se fazer antes dos testes, como importações
      });
   
      describe("#fatorial()", function (){
           test("igual a 1 para configurações de tamanho zero", function (){
              assert.equal(1, fatorial(0));
          });
   
          test("igual a 1 para configurações de tamanho um", function (){
              assert.equal(1, fatorial(1));
          });
   
          test("igual a 2 para configurações de tamanho dois", function (){
              assert.equal(2, fatorial(2));
          });
   
          test("igual a 6 para configurações de tamanho três", function (){
              assert.equal(6, fatorial(3));
          });
      });
   
      after(function () {
          // Qualquer coisa a se fazer depois dos testes serem finalizados
      });
  });

A principal diferença é somente a escrita. O BDD usa um estilo mais verboso então isso pode ser lido quase como uma sentença.

Isso é o mesmo que dizer que o BDD elimina problemas que o TDD venha a causar. A habilidade de ler seus testes como uma sentença é uma maneira mais cognitiva de como podemos pensar acerca dos mesmos testes. O argumento atua no "se você pode ler seus testes fluidamente, você naturalmente irá escrevê-los de uma forma melhor e mais compreensiva".

Apesar de esse exemplo ser muito simples e não ilustrar totalmente isso, os testes BDD devem estar focados nas funcionalidades, e não nos resultados (essa também é uma das principais diferenças entre os dois). Frequentemente, você ouvirá que o BDD existe para ajudar no design do software, e não testá-lo como o TDD é suposto de fazer.

DDD

DDD, ou Domain-Driven Development, é um conjunto de padrões e métodos que focam na elaboração de aplicações que se caracterizam por refletir a cobrança compreendida e satisfatória de um negócio qualquer. Analisando sobre outra ótica, ele constitui uma inovação na forma de formar qualquer pensamento sobre a prática de desenvolvimento e seus métodos.

O DDD lida com a construção de uma espécie de molde da realidade por inicialmente entendê-la por completo e só depois pôr terminologia, regras e meio lógico juntos de forma abstrata no código fonte, o que comumente se chama de modelo de domínio.

Não constitui nenhum framework ou ferramenta, mas sim uma espécie de plugin conceitual que pode ser acoplado ao projeto em qualquer momento.

O DD pode ser entendido também como uma forma de automatizar um negócio e seus processos, de forma que estejamos sempre focados no software em si para que só assim o negócio seja atendido por completo.

É comum, inclusive, associar o desenvolvimento de softwares corporativos a essa ideia em específico, mas o DDD vai além e mostra formas e padrões de conceitualizar tudo isso em um modelo de domínio.

TDD x BDD

A situação de ter de escolher entre TDD e BDD é complicada. Isso depende de haver ou não um framework de testes apropriado para a sua linguagem escolhida, o que os seus colegas de trabalho sentem em relação ao mesmo, e dependendo da situação muitos outros fatores entram na contagem.

Alguns defendem que o BDD é sempre melhor que o TDD porque ele tem a possibilidade de eliminar problemas que possam aparecer quando do uso do TDD.

A chave para o BDD é que ele pode prevenir esses problemas; isso não é garantido. Problemas como má organização do código, más práticas de design, etc. sempre irão persistir. Você deixará de ter de escrever testes ruins e em contrapartida terá funcionalidades mais robustas.

Particularmente, nenhum dos estilos é melhor que o outro, isso realmente depende da situação, e de quem usa. Alguém que sabe como escrever ótimos testes TDD pode ter alguns problemas como alguém que sabe escrever ótimos testes BDD. Se você se pegar escrevendo testes incompletos usando TDD e quiser um design de software melhor, então dê uma chance ao BDD.

Ainda mais processos

Existem ainda algumas outras terminologias usadas para classificar outras vertentes de teste usadas como processos nos projetos ágeis. Vejamos algumas delas abaixo:

  • FDD – Feature Driven Development (Desenvolvimento Guiado por Funcionalidades): O FDD é um tipo de teste feito para manter e criar projetos por intermédio de uma lista de atividades simples, de forma a incentivar o desenvolvimento de novos códigos, bem como compartilhar o conhecimento acerca dos mesmos. Isso permite, então, a principal meta do FDD seja alcançada no ciclo de vida do software: resultados contínuos, funcionais e tangíveis.
  • ATDD – Acceptance test-driven development (Desenvolvimento Guiado por Testes de aceitação): Conforme falamos anteriormente, esse tipo de teste comumente é confundido com os testes do tipo TDD, por causa da similaridade com que o desenvolvimento ágil associa os testes de aceitação aos mesmos. Nesse tipo de teste, o trabalho está totalmente direcionado aos testes de aceitação, o que significa que pode haver uma analogia entre este e o TDD.

Quando escrever testes unitários?

Nós já discutimos inúmeros conceitos relacionados à utilização dos testes unitários até aqui. Talvez a pergunta mais pertinente que possa surgir nesse momento, antes mesmo de entender e aceitar todos os conceitos até então apresentados, seja: Por que eu devo escrever testes unitários? Quando devo fazer isso?

Alguns desenvolvedores lidam com esse processo como mais uma prática chata a ser seguida dentro dos processos de uma empresa. Mais ainda, talvez antes de se fazer tais perguntas, sejam necessários antes um contato inicial com o ato de escrever testes.

Somente assim, você terá insumo suficiente para discernir se deve ou não escrever testes de unidade, e aí, então, teremos a primeira pergunta respondida. Mas e quanto ao “quando”?

É fácil para um gerente semi técnico dizer “escreva todos os testes do início” (em alguns casos, dizer isso pra você mesmo será tão fácil quanto), mas a verdade é que você frequentemente não saberá quais são os pequenos componentes que são necessários quando apresentados juntos a um caso de uso de nível considerável.

É comum, por exemplo, que desenvolvedores façam os seus testes à medida que se vai desenvolvendo o código, à medida que todos as “unidades” requeridas para implementar o caso de uso são feitas. Ao mesmo tempo, você estará constantemente mudando funções sobre esse código, refatorando e abstraindo também algumas partes, e tentando se antecipar ao que possivelmente irá causar perda de tempo durante esse processo.

Consideremos o seguinte cenário: “como um usuário, ao logar na aplicação você será levado para uma tela de menu principal”. Como podemos quebrar essa operação genérica em unidades?

Imediatamente podemos selecionar o “login de formulário” como uma implementação desse caso de uso que pode ser testada, e iremos adicionar algumas linhas de código de teste requeridas para implementar essa funcionalidade corretamente (Listagem 5).

Listagem 5. Exemplo de divisão dos passos para realizar o teste unitário.

  describe("login usuário", function() { 
    // crítico
    it("garanta que os endereços de email inválidos serão pegos", function() {});
    it("garanta que os endereços de email válidos irão passar da validação", function() {});
    it("garanta que o formulário de submissão modifique o caminho padrão", function() { });
   
    // é bom ter...
    it("garanta que o helper client-side helper seja exibido para campos vazios", function() { });
    it("garanta que ao pressionar enter no campo de senha envie o formulário", function() { });
  });

Dependendo das restrições implementadas, você deve decidir se deve ou não pular alguns passos, mas felizmente é possível ver com facilidade o quão crítico são cada um deles, isto é, que se eles falharem podem bloquear o uso da aplicação.

Um outro exemplo clássico alusivo é o da construção de um carro. Para construir um carro do início ao fim, cada parte que faz a motor, o chassi, os pneus, devem ser verificados individualmente para estar em ordem antes que elas possam de fato ser atreladas ao carro.

Se o tempo for dito insuficiente para averiguar todas estas peculiaridades, então possivelmente peças importantes serão despriorizadas e poderão não estar funcionando corretamente. Por exemplo, os pneus podem acabar não sendo verificados em relação à pressão, a costura interior pode não ser verificada contra danos.

Isso pode resultar em um carro falho, mas isso não seria seu problema, aparentemente. Apenas uma dica: verifique com cautela o que pode ou não ser deixado de lado no momento de testar.

Teste Unitário em JavaScript

Essa é uma história de controvérsias e recente. Apesar de JavaScript já ocupar espaço considerável na comunidade de desenvolvimento front-end, inclusive até quando comparada a outras linguagens de programação mais famosas como o Java, C# ou PHP, sendo considerada referência como linguagem, o JavaScript ainda tem muito a evoluir junto a suas companheiras de lado (client side).

É muito fácil, e comum, ignorar testes unitários quando se embarca em um projeto de grande escala que faz uso de JavaScript. Muitas vezes motivados pelo grande costume de desenvolver testes apenas no lado do servidor, uma vez que muitas das regras de validação são sempre duplicadas em ambos os lados cliente e servidor.

Porém, a necessidade de construir testes de unidade para JavaScript é tão real quanto com quaisquer outras linguagens. As ferramentas e procedimentos de testes unitários, por sua vez, não são tão claros para JavaScript quanto para as demais linguagens, o que acaba por construir uma imagem mais defensiva dos programadores para com o teste usando a linguagem de script.

O JavaScript vem de uma longa caminhada contra muitos conceitos, e anticonceitos. Houve um tempo onde ele era facilmente descartado, talvez adequado para validações não-críticas, mas não mais que isso. Ao longo dos anos, o pior obstáculo que programas feitos em JavaScript enfrentavam era sempre escrever aplicações em larga escala, ou ao menos mitigáveis.

Com o tempo, os dialetos JavaScript foram ficando mais consistentes entre os browsers do que eram antes, e ferramentas como o jQuery podem ajudar a deixar essas diferenças (que ainda existem) ainda mais leves aos olhos dos desenvolvedores. Os debuggers finalmente alcançaram melhores patamares de qualidade, jamais vistos antes (sim, debugar JavaScript era terrível), atraindo, assim, o que antes afastava.

NOTA: O debug se tornou melhor ao longo do tempo, mas ainda depende do browser usado, pois cada fabricante define suas próprias ferramentas de depuração.

Para todos os casos, com o advento da HTML5 e das plataformas mobile, o JavaScript está se tornando cada vez menos ignorável. Aplicações front-end inteiras estão sendo desenvolvidas em JavaScript, e enquanto a ferramenta suporta defasagens antes escondidas em linguagens como Java e C#, isso significa que temos evoluído.

E dentro desse escopo, uma área que permanece particularmente mudando, e que irá começar a ficar incrivelmente importante nos projetos JavaScript de escopo crescente são os testes unitários.

Para ilustrar uma situação mais próxima da programação, vejamos um exemplo de função básica em JavaScript, conforme o código a seguir:


function
addQuatroAoNumero(num){

 return num + 4;

}

A mesma função basicamente adiciona o valor 4, como um número, à variável passada por parâmetro. Na Listagem 6 podemos verificar então um possível caso de teste para uma função de execução.

Listagem 6. Caso de teste para a função addQuatroAoNumero().

  (function testAddQuatroAoNumero (){
      var num = 5, valorEsperado= 9;
      
      if (addQuatroAoNumero (num) === valorEsperado) {
          console.log("Passou!");
      } else {
          console.log("Falhou!");
      }
  }());

Após passar o valor 5 para a função sendo testada, o teste checa que o valor retornado é 9. Se o teste suceder, a mensagem “Passou!” é impressa no console de um browser moderno, caso contrário, a mensagem “Falhou!” aparecerá. Para executar este exemplo, você precisará efetuar dois passos básicos:

  1. Importar o arquivo js que contém o código apresentado nas duas listagens anteriores.
  2. Abrir a página da Listagem 7 no browser.
Listagem 7. Página HTML para execução do exemplo.

  <!DOCTYPE html>
  <html>
       <head>
           <meta http-equiv="Content-type" content="text/html; charset=utf-8">
           <title>Exemplo Teste Unitário JavaScript</title>
           <script type="text/javascript" src="js/script.js"></script>
       </head>
       <body></body>
  </html>

Ao invés de usar o console do browser para exibir os valores, você pode imprimir os mesmos dentro da página ou dentro de uma janela pop-up através da função alert() do próprio JavaScript.

Dentro do universo de teste unitário, os elementos usados para verificar se certa condição é satisfeita são chamados de “assertions” (afirmações). Na Listagem 6 o assertion está concentrado na operação:

 addQuatroAoNumero (num) === valorEsperado 

Para os casos em que se tem muitos assertions sendo feitos, é interessante fazer uso de um framework de teste unitário JavaScript para auxiliar.

Escolhendo um Framework

Se você desenvolve com Java, você provavelmente não perde muito tempo decidindo que framework de teste unitário irá usar. O mais famoso deles, o jUnit, é sempre a opção preferida da maioria dos desenvolvedores por razões óbvias de qualidade. O panorama do JavaScript não é tão resolvido assim, e que framework escolher pode afetar profundamente as suas capacidades de testes.

Vejamos pois alguns dos frameworks mais famosos para JavaScript, bem como suas particularidades que ajudarão a analisar quando adotar um ou outro.

qUnit

O qUnit é um ótimo candidato para começar. Antes de tudo, ele é consideravelmente fácil de aprender. De início pode ser um pouco complicado para entender alguns comportamentos que são menos intuitivos, mas assim que se entende os problemas por trás destes comportamentos tudo começa a fluir mais rapidamente.

Ele possibilita interação com o DOM; você pode, por exemplo, identificar uma DIV na sua página de teste para ser resetada para seu estado original entre os testes, promovendo assim atomicidade aos testes. Além disso, ele também provê suporte a “testes assíncronos”, o que constitui uma funcionalidade vital para suas aplicações.

js-test-driver

O js-test-driver foca no teste paralelo em múltiplos browsers. Ele tem sua própria linguagem de casos de teste e assertions, distinta do qUnit. Existem projetos que possibilitam ambos de serem usados juntos, apesar de não existir um que suporte todas as capacidades de ambos frameworks.

Diferente do qUnit, ele não usa partes do DOM para relatar resultados e reinicia o DOM inteiro entre um teste e outro. Isso impõe uma restrição a ser testada.

Ambiente de Execução dos Testes

Essa é outra questão que parece muito mais complicada com JavaScript do que com Java: em qual ambiente devo executar meus testes?

Você deve sempre querer que o seu ambiente de teste seja compatível com o ambiente de produção o mais próximo possível. Mas para muitas aplicações JavaScript, um pedaço crítico desse ambiente, o browser com seu respectivo motor JavaScript, é escolhido pelo usuário.

Mesmo se os problemas cross-browser (vários browsers) não forem tão ruins quanto eles eram antes, ainda é muito imaturo dizer que um teste feito em um browser terá um resultado consistente por além dos demais.

Ao mesmo tempo, testar em todos os diferentes browsers traz mais desafios quando você começar a pensar em testes automatizados e CI. Esse é o grande desafio que o js-test-driver é designado a resolver. Se você quer uma solução puramente qUnit, você provavelmente irá encontrar por si só escrevendo seus próprios scripts ou fazendo sem automação.

Existem vários ambientes de execução JavaScript com suporte fácil a scripts, como o NodeJE, por exemplo, que será muito útil para alguns testes automatizados de alto nível. Porém, eles terão suas próprias limitações (NodeJS não tem suporte a DOM nativo, apesar dos add-ons existirem) e em todo caso não conseguirá simular o comportamento do browser.

Uma abordagem multicamada deveria se aplicar melhor às suas necessidades. Você poderia criar alguns scripts de teste para executar no NodeJS por CI, e então rodar periodicamente uma suíte de testes em cada browser alvo. Alguns bugs deverão não ser pegos como seriam se usasse uma CI completa.

Estrutura do Código

Nos primórdios do jUnit, não era incomum encontrar código Java que não se dirigisse diretamente aos testes unitários. Ao longo dos anos, isso passou a ser visto como um teste de qualidade de um design. O JavaScript tem, como melhor, começado a desenvolver essa disciplina, e algumas das funcionalidades da linguagem que empurram os desenvolvedores Java na direção certa (como encapsulamento) não gozam do mesmo nível de suporte nativo no JavaScript.

Existem alguns frameworks que podem ajudar na escrita de testes unitários. O ExtJS, por exemplo, põe um sistema de classes no topo do modelo de objetos do JavaScript. Ferramentas como essa podem ajudar se você as permitir, porém como elas não fazem parte da linguagem, você pode sempre escolher ir contra as mesmas.

Escrever JavaScript testável é, e para o futuro continuará sendo, uma arte a ser modelada de acordo com a prática.

Não é algo simples manter sempre o foco na testabilidade. Mesmo que você programa todas as linhas do seu código com os olhos voltados para a testabilidade do mesmo, sempre aparecerão determinados comportamentos no código que fugirão à regra, como ubiquidade ou comportamentos assíncronos.

Todo motor JavaScript é single-threaded, mesmo que algumas tarefas simples como requisitar um dado do servidor (usando Ajax, por exemplo), respondendo à chamada UI, ou explicitamente retardando uma atividade (através de um setTimeout ou setInterval), são todas feitas assincronamente.

Então, o que um executor de testes deve fazer? Você é chamado a agrupar o código que está testando.

  1. O framework é configurado para executar os testes;
  2. O código de teste configura pré-condições e faz uma chamada para o código a ser testado;
  3. O código de teste checa os assertions;
  4. O framework reporta os resultados e vai para o próximo teste.

Mas e se o código a ser testado faz alguma coisa de forma assíncrona? Então, podemos verificar alguns outros passos específicos:

  1. O framework é configurado para executar os testes;
  2. O código de teste configura pré-condições e faz uma chamada para o código a ser testado;
  3. O código a ser testado inicia algo, e então ele chama alguma função para finalizar a tarefa;
  4. O framework reporta (imprecisamente) os resultados e vai para o próximo teste;
  5. Outras funções de fila devem executar;
  6. A parte em fila do código a ser testada recebe sua vez de executar, e finaliza a coisa toda que foi iniciada no passo 3;
  7. O código de teste verifica os assertions.

Dessa forma você consegue uma porção de testes falhando por uma razão não muito boa.

O qUnit endereça isso com a função asyncronousTest(). Isso funciona apenas como a função regular test(), exceto pelo fato do framework não assumir que o teste está finalizado quando o código de teste retorna. Você deve informar a ela quando o teste está finalizado chamando a função start().

Esse tipo de comportamento soa um pouco retrógrado. Você chama a função start() quando você precisa finalizar o teste, porque isso diz ao framework para reiniciar o processamento. Esse mecanismo inteiro faz com que os testes executem em lockstep, o que parece que é síncrono, mas você pode chamar asyncronousTest() porque o nome referencia a natureza assíncrona do código sendo testado.

Objetos Mock

A natureza dinâmica do JavaScript traz, por sua vez, vários benefícios e flexibilidades para escrever objetos mock, mesmo usando um framework de objetos mock.

Em Java, o seu objeto mock tem que compartilhar uma interface com, e/ou ser uma subclasse de, a classe que o substitui. Dependendo da sofisticação do seu framework mock, isso pode impor várias restrições e/ou fazer com que o desenvolvedor pule alguns passos para ir direto ao trabalho pronto.

Em JavaScript, você pode criar um objeto que implemente exatamente o mesmo comportamento do mock. De certa forma isso também ajuda se o seu código tiver sido escrito com algum tipo de inversão de controle embutida, caso contrário ao menos você irá se beneficiar com a forma fácilo de interceptar uma função chamada no JavaScript.

Desenvolver código unitário não é uma tarefa simples, tampouco personalizável e imediatista como muitos desenvolvedores a consideram. Exige conhecimentos acerca do processo, técnicas e intimidade com as ferramentas e processos envolvidos. Existe, sobretudo, experiência que, por sua vez, só pode ser adquirida com o tempo e o acúmulo de situações durante a vida como desenvolvedor.

Confira também

Teste unitário para JavaScript, então, é mais complicado de assimilar principalmente em vista do histórico que a linguagem traz consigo, não sendo considerada por muito tempo sequer para programação básica, no até então advento das tecnologias server side.

Esse artigo é extremamente útil aos desenvolvedores que ainda têm dúvidas tanto à opção por escrever testes unitários para código JavaScript, como na hora de escolher um framework que atenda às necessidades sempre tão específicas de cada projeto.

Através de exemplos práticos examinaremos quais são as medidas mais adequadas para determinar qual framework melhor se encaixa na realidade de negócio de cada projeto, bem como analisaremos fatores muito importantes que diferem um framework de outro e o encaixam dentro de categorias específicas. Veremos também como migrar o seu código entre os mesmos frameworks, dadas as condições de configuração impostas por eles ante a conceitos como acoplamento, coesão e otimização.

Desenvolvimento front-end e testes unitários são dois termos extremamente recentes no universo de desenvolvimento de software se comparados a tantos outros que já estão bem fixados e definidos no mesmo meio. E quando falamos em unir ambas as tecnologias, chegamos em um ponto, muitas vezes, definido como tabu: será que realmente funciona implementar testes unitários em ambientes, linguagens e plataformas front-end? A resposta é mais que sim.

Nesta segunda parte do curso vamos explorar os frameworks JavaScript de testes de unidade que fornecem os recursos e atalhos necessários para simplificar mais ainda o tempo de desenvolvimento.

Testes e Frameworks

Você provavelmente sabe que o teste é bom, mas o primeiro obstáculo a superar quando se tenta escrever testes de unidade para o código do lado do cliente é a falta de quaisquer unidades reais. O código JavaScript é escrito para cada página de um site ou de cada módulo de uma aplicação e está intimamente misturado com a lógica de back-end e HTML. No pior dos casos, o código é completamente misturado com a HTML, como nos manipuladores de eventos in-line. Este é provavelmente o caso de quando nenhuma biblioteca JavaScript está sendo usada para qualquer abstração DOM. Escrever manipuladores de eventos em linha é muito mais fácil do que usar as APIs DOM para vincular esses eventos. Mais e mais desenvolvedores estão pegando uma biblioteca como jQuery para lidar com a abstração do DOM, o que lhes permite mover os eventos in-line para os scripts específicos, ambos na mesma página ou até mesmo em um arquivo JavaScript separado. No entanto, colocar o código em arquivos separados não significa que ele está pronto para ser testado como uma unidade.

Mas afinal, o que é uma unidade? Na melhor das hipóteses, é uma função pura que você pode lidar de alguma forma: uma função que sempre lhe dá o mesmo resultado para uma dada entrada. Isso faz com que o teste de unidade fique muito mais fácil, mas na maior parte do tempo você precisará lidar com os efeitos colaterais, que aqui significam manipulações DOM.

De um jeito ou de outro, ainda é muito útil descobrir quais unidades podemos usar para estruturar o nosso código e para construir testes de unidade em conformidade.

Escolhendo um framework de testes

Existe uma série de propriedades a serem consideradas quando da escolha de um framework de testes, e este é um ponto extremamente necessário, pois tal escolha irá influenciar na forma como se programa os testes, os limites que os mesmos terão (dentre os quais podemos encontrar limites de sintaxe, recursos, integrações, etc.), bem como outros fatores como documentação, apoio da comunidade e principalmente a força do framework no que diz respeito à certeza de que ele não será descontinuado.

As IDEs JavaScript não oferecem a mesma ajuda como as IDEs para linguagens tipadas e estáticas como o Java e o C#. Isso também impede a comunidade de padronizar uma estrutura de testes simples que possa oferecer recursos facilmente adaptáveis aos diferentes ambientes de execução.

Além disso, a depuração no passado sempre foi feita manualmente através de mensagens de alerta e extensões do navegador como o Firebug, por exemplo. Para o TDD é importante que a linguagem e o ambiente suportem padrões de teste, debug e refatoração, afim de perceber os padrões de projeto e teste como um todo.

Sintaxe

À medida que se investiga diferentes estruturas de teste, podemos notar que a sintaxe é um fator de diferenciação em todos eles. Este fator é bem pessoal e realmente vai depender do que faz o desenvolvedor se sentir confortável quando está utilizando uma ferramenta dessas. Isso vale não somente para frameworks de testes unitários, mas também para quaisquer tecnologias que façam uso de um código de comunicação, como linguagens de programação, ferramentas de gerenciamento de redes, dentre outros exemplos possíveis.

Por exemplo, o QUnit é mais que um framework de teste declarativo, pois sua API consiste de funções chamadas test, equals, strictEqual, deepEqual, etc.

Outra estrutura de teste, o Mocha, é mais parecido com o rspec (BOX 1) em que se lê de forma mais sentencial do que estrutural. Sua API consiste de funções chamadas describe, it, assert.equal, etc.

BOX 1. Rspec

É ferramenta de teste para a linguagem de programação Ruby.Nascido sob a bandeira do Desenvolvimento Behaviour-Driven, ele é projetado para fazer desenvolvimento orientado a testes uma experiência produtiva e agradável.

A sintaxe de um framework é criada para suportar uma metodologia escolhida, que em teoria será o TDD ou BDD (vistos no primeiro artigo deste curso).

A sintaxe TDD é puro JavaScript com métodos de biblioteca similares aos frameworks xUnit. Veja, por exemplo, o código apresentado na Listagem 1 que exibe um exemplo no JSTestDriver. Repare que as sentenças assert são típicas dessa sintaxe.

Listagem 1. Exemplo da sintaxe TDD com JSTestDriver.
TestCase (" FizzBuzzDevMedia ", {
    " test on 0 return 0": function () {
      var result = fizzbuzz (0) ;
      assertEquals (" Zero " ,0 , result ) ;
    }
  };

As sintaxes BDD são mais diversificadas, mas elas têm em comum o fato de estarem mais próximas de uma linguagem natural, já que este é um dos princípios do BDD. Veja nas Listagens 2 e 3 dois exemplos de diferentes sintaxes BDD.

Listagem 2. Exemplos da sintaxe BDD com Jasmine e JSpec BDD DSL.
describe (" FizzBuzz ", function () {
   it(" return 0 on 0", function () {
      expect ( fizzbuzz (0) ) . toEqual (0) ;
   });
  });
Listagem 3. Exemplos da sintaxe BDD com JSpec BDD DSL.
describe ’FizzBuzz ’
   it ’should return null on null ’
     fizzbuzz (0) . should . equal 0
   end
  end

Na Listagem 2 nós temos uma sintaxe JavaScript, mas os métodos de biblioteca são nomeados muito proximamente aos princípios do BDD. A sentença assertEqual() antes usada na Listagem 1, agora se apresenta como expect().toEqual(), o que é mais legível.

Na Listagem 3 temos uma DSL (Custom Domain-Specific Language) que pertence ao JSpec. Aqui, outro passo importante é tomado para traduzir para uma linguagem mais natural.

Características

Quando você começa a testar em JavaScript, vai rapidamente encontrarse em situações onde precisa-se que o framework de teste ajude o desenvolvedor a testar certos casos de uso. Nesse mesmo universo, alguns termos característicos são usados para ajudar a identificar tais cenários. Por exemplo, vejamos a lista de terminologias a seguir:

  • Spies – São estruturas usadas para ajudar a detectar de onde o código chamou uma determinada função. Isso é útil para identificar a origem de erros e bugs que são encontrados no momento do teste;
  • Stubs – Os stubs são muito semelhantes aos spies, exceto pelo fato de que eles contêm o comportamento já pré-definido. Por exemplo, suponha que se quer um método que espie algum código em específico e quando o mesmo for chamado, retorne ‘abc’. Com os stubs esse tipo de comportamento é possível;
  • Servidor Fake – Os servidores fake servem para facilitar o processo de simulação do envio de requisições que seria feito em um servidor real. Obviamente, como se trata de JavaScript, a única forma de fazer isso seria usando AJAX. Se o código emite chamadas AJAX e se quer falsificar a resposta da chamada AJAX, um servidor falso irá ajudá-lo mais facilmente a testar isso;
  • Relógio Fake – Da mesma forma, podemos criar relógios fake para simular o comportamento do código ante mudanças no relógio usado pelo ambiente para extrair valores como data, tempo, intervalos, contadores, etc. Se o código reage de forma diferente baseado na data ou hora, então um relógio falso irá ajudá-lo a controlar exatamente qual é a hora, enquanto se faz os testes.

Suporte

O framework de teste é apoiado pela comunidade? Essa é uma das primeiras perguntas que se deve fazer quando se escolhe um. Por exemplo, o grunt (a ferramenta de ambiente de construção) suporta a execução do QUnit em um servidor PhantomJS. Isto torna mais fácil de escrever testes e tê-los executados automaticamente toda vez que se salvar o arquivo de teste.

Outra pergunta interessante é verificar se seria prático excluir os arquivos do projeto quando não quiser mais usá-los? Isso facilitaria a vida no momento em que optasse por trocar de framework, migrá-lo ou até mesmo combiná-lo com outros. Esse grau de granularidade determina se o framework é altamente acoplado ou não.

Ambiente

O ambiente de execução mais comum para as aplicações JavaScript é o browser, e em muitas situações o próprio JavaScript dependerá do objeto DOM dos browsers para executar de forma correta. Porém, pode-se executar os seus testes em qualquer ambiente que simule todas as características de uma engine de browser para JavaScript.

A ideia da execução fora do browser tornar o JavaScript mais útil tem ganhado popularidade à medida que as linguagens e aplicações server-side tem ficado maiores e mais complexas. Até esse ponto, a lógica interna das aplicações pode ser verificada em um ambiente que simule o JavaScript como se ele estivesse no lado cliente da execução ou um CMS (Content Management Systems), por exemplo, que irá lidar com toda a comunicação com o DOM por si só.

Atualmente, o Mozilla suporta o Rhino, por exemplo, como uma engine JavaScript implementada em Java. O Rhino provê uma execução generalizada do modelo JavaScript sem a API do browser DOM, o que facilita os testes e torna o mesmo apto a ser usado como ambiente padrão de testes.

Execução

A principal diferença quando estamos falando sobre execução dos testes, seria se o programador precisar consultar o browser ou se não receber nenhum resultado do mesmo. Efetuar um simples refresh na página do navegador para cada teste executado é algo fácil de implementar e configurar, mas não é rápido e simples o suficiente como o processo do TDD dita.

O problema irá crescer de acordo com o número de navegadores que precisam executar os testes, uma vez que todos eles precisarão ser abertos e recarregados automaticamente. A vantagem desse método é a transparência oferecida, pois a biblioteca por si só será um arquivo .js, facilitando o processo de exploração e extensão usando técnicas já conhecidas. Muitos dos executores de testes utilizarão esse tipo de teste in-browser, criando uma página HTML onde os arquivos de teste de código serão carregados através de tags script. Para bibliotecas de teste é possível que programadores criem seus próprios testes in-browser através do uso de métodos das mesmas bibliotecas.

A alternativa para esse tipo de estratégia é chamada de testes headless, que implica que os testes são executados a partir da linha de comando ou de dentro da IDE e os resultados são retornados para a interface. Eles podem ser executados em diferentes ambientes, alguns em Rhino, enquanto outros empurram os testes para um browser e exibem apenas os resultados retornados.

Por outro lado, esse tipo de teste pode ter mais demanda de configuração com atenção especial para as configurações locais de servidores e potenciais conexões a browsers ou máquinas remotas. Os arquivos de teste e código precisam de certas configurações para garantir as referências corretas e, em alguns casos, monitorar operações de pesquisa para as alterações de arquivos.

Biblioteca de Testes

A biblioteca de testes decide como o teste é feito, o que pode ser feito e como as demandas serão atreladas ao programador.

A biblioteca consiste de métodos para determinar o ciclo de vida de configuração, testes individuais, assim como agrupar os testes em suítes. Ela também tem métodos de comparação, chamados de mecanismos de asserção no TDD e qualificadores no BDD, o que determina a saída dos testes. O alcance e cobertura dos métodos de asserção e o ciclo de vida de configuração entre as bibliotecas pode variar, mas em geral como eles serão construídos depende dos objetivos de design suportados pela metodologia usada.

Quando estes fatores são cobertos, uma biblioteca de testes pode adicionar funcionalidades para garantir um teste facilitado, alta legibilidade e menos repetições para o desenvolvedor.

Com base em todos os fatores descritos, neste artigo iremos trabalhar essencialmente com três frameworks de testes: o QUnit, o YUI Test e o JSTestDriver. Eles representam os três frameworks mais usados e mais maduros em todos os fatores apresentados no momento, além de uma vasta documentação e atualizações constantes da comunidade.

Construindo os testes

Este artigo é para ajudá-lo com o problema mais difícil: extrair o código existente e testar as partes importantes, descobrindo e corrigindo bugs no código pelo meio do caminho.

O processo de extrair o código e colocá-lo em uma forma diferente, sem modificar o seu comportamento atual, é chamado de refatoração (ou refactoring). O refactoring é um excelente método de melhorar o design do código de um programa, porque qualquer mudança realmente pode modificar o comportamento do mesmo, sendo mais seguro então fazer somente quando os testes unitários estiverem prontos.

Este problema quer dizer que para adicionar testes ao código existente você tem que correr o risco de quebrar as coisas. Então, até que você tenha cobertura sólida com testes de unidade, você precisa continuar testando manualmente para minimizar esse risco.

Vejamos um exemplo prático na Listagem 4 que exemplifica uma situação onde temos um código JavaScript básico com uma função nossaData() que irá receber um parâmetro com a mesma data a ser processada. A função, por sua vez, irá capturar a data fornecida e verificar, através da criação de um novo objeto de data atual, a diferença de tempo entre uma e outra. O resultado será impresso na variável “diferença_dias” que irá guardar a mensagem correspondente ao total da diferença calculada entre ambas as datas.

Listagem 4. Exemplos de código de teste – busca por títulos.
<!DOCTYPE html>
  <html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>Exemplos de data mutilados</title>
      <script>
      function nossaData(time){
          var data = new Date(time || ""),
              diff = ((new Date().getTime() - data.getTime()) / 1000),
              diferenca_dias = Math.floor(diff / 86400);
   
          if (isNaN(diferenca_dias) || diferenca_dias < 0 || diferenca_dias >= 31) {
              return;
          }
   
          return diferenca_dias == 0 && (
                  diff < 60 && "Agora" ||
                  diff < 120 && "1 minuto atrás" ||
                  diff < 3600 && Math.floor( diff / 60 ) + " minutos atrás" ||
                  diff < 7200 && "1 hora atrás" ||
                  diff < 86400 && Math.floor( diff / 3600 ) + " houras atrás") ||
              diferenca_dias == 1 && "Ontem" ||
              diferenca_dias < 7 && diferenca_dias + " dias atrás" ||
              diferenca_dias < 31 && Math.ceil( diferenca_dias / 7 ) + " semanas atrás";
      }
      window.onload = function(){
          var links = document.getElementsByTagName("a");
          for (var i = 0; i < links.length; i++) {
              if (links[i].title) {
                  var data = nossaData(links[i].title);
                  if (data) {
                      links[i].innerHTML = data;
                  }
              }
          }
      };
      </script>
   </head>
   <body>
         <ul>
         <li class="entry" id="post57">
               <p>abc abc acb acb …</p>
               <small class="extra">
                      Postado em <a href="/2014/12/abc/57/" title="2014-12-02T20:24:17Z">
                         02 de Dezembro de 2014</a>
                      por <a href="/john/">Sueila Valente</a>
               </small>
         </li>
         <!-- Mais itens -->
         </ul>
   </body>
  </html>

Se você executar esse exemplo, verá um problema: nenhuma das datas são substituídas. O código funciona, no entanto ele percorre todas as âncoras na página e checa por uma propriedade de título em cada uma. Se não houver uma, ele passa para a função nossaData(), e se ela retornar um resultado, ele atualiza o innerHTML do link com o resultado.

Além disso, temos um outro problema: para qualquer data anterior a 31 dias, a função nossaData() apenas retornará “undefined” (implicitamente, com um simples return), deixando o texto da âncora como está. Então, para ver o que deveria acontecer, nós podemos programar uma data hardcoded (fixa no código). Veja na Listagem 5 o resultado da nossa página após as alterações.

Listagem 5. Código de teste após alterações de data.
<!DOCTYPE html>
  <html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>Exemplos de data mutilados</title>
      <script>
      function nossaData(time, time){
          var data = new Date(time || ""),
              diff = ((new Date().getTime() - data.getTime()) / 1000),
              diferenca_dias = Math.floor(diff / 86400);
   
          if (isNaN(diferenca_dias) || diferenca_dias < 0 || diferenca_dias >= 31) {
              return;
          }
   
          return diferenca_dias == 0 && (
                  diff < 60 && "Agora" ||
                  diff < 120 && "1 minuto atrás" ||
                  diff < 3600 && Math.floor( diff / 60 ) + " minutos atrás" ||
                  diff < 7200 && "1 hora atrás" ||
                  diff < 86400 && Math.floor( diff / 3600 ) + " houras atrás") ||
              diferenca_dias == 1 && "Ontem" ||
              diferenca_dias < 7 && diferenca_dias + " dias atrás" ||
              diferenca_dias < 31 && Math.ceil( diferenca_dias / 7 ) + " semanas atrás";
      }
      window.onload = function(){
          var links = document.getElementsByTagName("a");
          for (var i = 0; i < links.length; i++) {
              if (links[i].title) {
                  var data = nossaData("2014-12-02T20:24:17Z", links[i].title);
                  if (data) {
                      links[i].innerHTML = data;
                  }
              }
          }
      };
      </script>
   </head>
   <body>
         <ul>
         <li class="entry" id="post57">
               <p>abc abc acb acb …</p>
               <small class="extra">
                      Postado em <a href="/2014/12/abc/57/" title="2014-12-02T20:24:17Z">02 de Dezembro de 2014</a>
                      por <a href="/john/">Sueila Valente</a>
               </small>
         </li>
         <!-- Mais itens -->
         </ul>
   </body>
  </html>

Perceba que nós mantemos o comportamento criado na Listagem 4, porém com a adição de uma data fixa na chamada da função dentro do onLoad da página. Essa adição poderá ser feita quantas vezes forem necessárias pelo testador-programador, de modo a atingir os diversos cenários de teste.

Agora, as ligações devem dizer "2 horas atrás", "Ontem" e assim por diante. Isso já é alguma coisa, mas ainda não é uma unidade testável real. Mesmo que tudo isso funcione, qualquer pequena alteração para a marcação provavelmente quebrará o teste, resultando em uma relação custo-benefício muito ruim para um teste como esse.

Em vez disso, vamos refatorar o código apenas o suficiente para ter algo que possamos testar a unidade.

Precisamos fazer duas mudanças para que isso aconteça:

  1. passar a data atual para a função nossaData() como argumento, em vez de ter que usar apenas new Date e;
  2. extrair a função para um arquivo separado, para que possamos incluir o código em uma página separada para os testes de unidade.

Remova o conteúdo da tag script interna à tag head e adicione o mesmo em um novo arquivo .js. Logo após, altere o nosso código fonte para o que está na Listagem 6.

Listagem 6. Código de teste refatorado.
<!DOCTYPE html>
  <html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>Exemplos de data mutilados</title>
      <script src="nossadata.js"></script>
      <script>
      window.onload = function() {
          var links = document.getElementsByTagName("a");
          for ( var i = 0; i < links.length; i++ ) {
              if (links[i].title) {
                  var date = nossaData("2014-12-02T20:24:17Z", links[i].title);
                  if (date) {
                      links[i].innerHTML = date;
                  }
              }
          }
      };
      </script>
   </head>
   <body>
         <ul>
               <li class="entry" id="post57">
                      <p>abc abc acb acb …</p>
                      <small class="extra">
                             Postado em <a href="/2014/12/abc/57/" title="2014-12-02T20:24:17Z">02 de Dezembro de 2014</a>
                             por <a href="/john/">Sueila Valente</a>
                      </small>
               </li>
               <!-- Mais itens -->
         </ul>
   </body>
  </html>

Na listagem tem o código com a melhoria explícita da adição do tag de script que agora importa o arquivo de JavaScript dinamicamente quando a página for executada. Com esta abordagem também não é preciso se preocupar com nenhuma mudança no HTML, pois o mesmo conseguirá enxergar as funções como antes.

Agora que temos algo para testar manualmente, vamos escrever alguns testes unitários e discutir logo em seguida, como mostra a Listagem 7.

Listagem 7. Testes de Unidade para o exemplo criado.
<!DOCTYPE html>
  <html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>Exemplos de data mutilados</title>
      <script src="prettydate.js"></script>
      <script>
      function test(valor, esperado) {
          resultados.total++;
          var result = prettyDate("2014-12-02T22:25:00Z", valor);
          if (result !== esperado) {
              resultados.bad++;
              console.log("Esperado " + esperado + ", mas foi " + result);
          }
      }
      var resultados = {
          total: 0,
          bad: 0
      };
      test("2014/12/02 22:24:30", "Agora");
      test("2014/12/02 22:23:30", "1 minuto atrás");
      test("2014/12/02 21:23:30", "1 hora atrás");
      test("2014/12/01 22:23:30", "Ontem");
      test("2014/11/30 22:23:30", "2 dias atrás");
      test("2014/11/26 22:23:30", undefined);
      console.log("De " + resultados.total + " testes, " + resultados.bad + " falharam, "
          + (resultados.total - resultados.bad) + " passaram.");
      </script>
   </head>
   <body>
         <!-- ... -->
   </body>
  </html>

Isto irá criar um framework de testes ad-hoc, usando apenas o console para a saída. Não tem nenhuma dependência ao DOM em tudo, então poderemos muito bem executá-lo em um ambiente JavaScript não-navegador, como o Node.js ou Rhino, extraindo o código na tag script para o seu próprio arquivo.

Se um teste falhar, irá produzir o esperado e real resultado para esse teste. No final, ele vai mostrar um resumo do mesmo com o número total de testes, os que falharam e os que foram bem-sucedidos.

Se todos os testes passarem você verá o seguinte no console:

De 6 testes, 0 falhou, 6 passaram. 

Para ver como uma asserção falha se parece, nós podemos mudar alguma coisa no teste para quebrá-lo e ter algo como:

Esperado 2 dias atrás, mas foi 2 dias atrás.
  De 6 testes, 1 falhou, 5 passaram. 

Embora essa abordagem ad-hoc seja interessante como uma prova de conceito (você realmente pode escrever um executor de teste em apenas algumas linhas de código), é muito mais prático usar um framework de testes unitários existente que proporciona uma melhor saída e mais infraestrutura para a escrita e organização de testes.

QUnit JavaScript

O QUnit é um framework JavaScript de testes unitários easy-to-use (fácil para uso) poderoso. É usado pelo jQuery, jQuery UI e o projeto jQuery Mobile e é capaz de testar qualquer código JavaScript genérico, incluindo o próprio.

Para usar o QUnit no seu projeto você precisa:

  1. Baixar o arquivo qunit.css e o arquivo qunit.js (ver seção Links), diretamente do site oficial do projeto.
  2. Criar uma página HTML contendo tags específicas que importam os arquivos de CSS e o JavaScript que você baixou.

Veja na Listagem 8 como ficaria o nosso código de exemplo após as devidas alterações incluindo o QUnit.

Listagem 8. Código de exemplo após inclusão do QUnit.
<!DOCTYPE html>
  <html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>Exemplos de data mutilados refatorados</title>
         
      <script src="nossadata.js"></script>
         <script src="qunit.js"></script>
      <script>
               test("nossadata basics", function() {
                      var now = "2014-12-02 22:25:00";
                      equal(nossaData(now, "2014/12/02 22:24:30", "Agora"));
                      equal(nossaData(now, "2014/12/02 22:23:30", "1 minuto atrás"));
                      equal(nossaData(now, "2014/12/02 21:23:30", "1 hora atrás"));
                      equal(nossaData(now, "2014/12/01 22:23:30", "Ontem"));
                      equal(nossaData(now, "2014/11/30 22:23:30", "2 dias atrás"));
                      equal(nossaData(now, "2014/11/26 22:23:30", undefined));
               });
      </script>
   </head>
   <body>
      <div id="qunit"></div>
   </body>
  </html>

Junto com o clichê HTML de costume, temos três arquivos incluídos: dois arquivos para o QUnit (qunit.css e qunit.js) e os nossadata.js anterior.

Então, há um outro bloco de script com os testes reais. O método “test” é chamado uma vez, passando uma string como primeiro argumento (nomeação do teste) e passando uma função com o segundo argumento (que irá executar o código real para este teste). Este código define o variável “now“, que é reutilizada abaixo, e em seguida, chama o método “equal” algumas vezes com argumentos variados. O método de igualdade é uma das várias asserções que o QUnit oferece. O primeiro argumento é o resultado de uma chamada para nossaData(), com a variável “now” como o primeiro parâmetro e uma sequência de datas como segundo. O segundo argumento para “equal” é o resultado esperado. Se os dois argumentos para “equal” são o mesmo valor, então a asserção vai passar; caso contrário, ela irá falhar.

Finalmente, no elemento do corpo temos um código específico do QUnit. Estes elementos são opcionais. Se estiver presente, o QUnit irá utilizá-los para a saída dos resultados do teste, como podemos ver na Figura 1.

Resultado da execução dos testes
Figura 1. Resultado da execução dos testes – Interface do QUnit.

Com uma execução falha, teríamos algo parecido com a Figura 2.

Resultado da execução falha dos testes
Figura 2. Resultado da execução falha dos testes – Interface do QUnit.

Como o teste contém uma asserção falhando, o QUnit não esconde os resultados para esse teste e podemos ver imediatamente o que deu errado. Junto com a saída dos valores previstos e reais temos uma comparação entre os dois, o que pode ser útil para a comparação de sequências maiores. Aqui, é bastante óbvio que deu errado.

Continuando a fase de refactorings, as asserções estão atualmente bastante incompletas, porque ainda não estamos testando as n semanas anteriores à variante. Antes de adicioná-lo, devemos considerar a refatoração do código de teste. Atualmente, estamos chamando nossaData() para cada afirmação e passando o argumento now. Poderíamos facilmente refatorar isso em um método de asserção customizado, como mostrado na Listagem 9.

Listagem 9. Método de assetion nossaData refatorado.
test("nossadata basics", function() {
      function date(then, expected) {
          equal(nossaData("2014/12/02 22:25:00", then), expected);
      }
      date("2014/12/02 22:24:30", "Agora"));
      date("2014/12/02 22:23:30", "1 minuto atrás"));
      date("2014/12/02 21:23:30", "1 hora atrás"));
      date("2014/12/01 22:23:30", "Ontem"));
      date("2014/11/30 22:23:30", "2 dias atrás"));
      date("2014/11/26 22:23:30", undefined));
  });

Nessa listagem extraímos a chamada para nossaData() na função date, colocando a variável now para dentro da função. Terminamos com apenas os dados relevantes para cada afirmação, tornando-se mais fácil de ler, enquanto a abstração subjacente permanece bastante óbvia.

Testando a Manipulação do DOM

Agora que a função nossaData() foi testada o suficiente, vamos focar um pouco no exemplo inicial. Ao longo das mudanças na função nossaData(), o QUnit também selecionou alguns elementos do DOM e atualizou-os dentro do evento de load “window”. Aplicando os mesmos princípios de antes, nós devemos estar aptos a refatorar o código e testá-lo. Além disso, vamos introduzir um módulo para essas duas funções, para evitar desordenar o namespace global e para sermos capazes de dar a essas funções individuais nomes mais significativos.

Vejamos então os códigos representados nas Listagens 10 e 11, que retratam esse cenário de mudanças.

Listagem 10. Mudanças de nomenclaturas no código.
<!DOCTYPE html>
  <html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
      <title>Exemplos de data mutilados refatorados</title>
         
      <script src="nossadata.js" type="text/javascript"></script>
         <script src="qunit.js" type="text/javascript"></script>
         <link href="qunit.css" rel="stylesheet"/>
      <script>
               test("nossadata.format", function() {
                      function date(then, expected) {
                             equal(nossaData("2014/12/02 22:25:00", then), expected);
                      }
                      date("2014/12/02 22:24:30", "atrásra"));
                      date("2014/12/02 22:23:30", "1 minuto atrás"));
                      date("2014/12/02 21:23:30", "1 hora atrás"));
                      date("2014/12/01 22:23:30", "Ontem"));
                      date("2014/11/30 22:23:30", "2 dias atrás"));
                      date("2014/11/26 22:23:30", undefined));
               });
               
               test("nossaData.update", function() {
                      var links = document.getElementById("qunit-fixture").getElementsByTagName("a");
                      equal(links[0].innerHTML, "28 de Janeiro de 2008");
                      equal(links[2].innerHTML, "27 Janeiro de 2008");
                      nossaData.update("2014/12/02 22:25:00Z");
                      equal(links[0].innerHTML, "2 horas atrás");
                      equal(links[2].innerHTML, "Ontem");
               });
   
               test("nossaData.update, one day later", function() {
                      var links = document.getElementById("qunit-fixture").getElementsByTagName("a");
                      equal(links[0].innerHTML, "28 de Janeiro de 2008");
                      equal(links[2].innerHTML, "27 de Janeiro de 2008");
                      nossaData.update("2014/12/02 22:25:00Z");
                      equal(links[0].innerHTML, "Ontem");
                      equal(links[2].innerHTML, "2 dias atrás");
               });
               </script>
      </script>
    </head>
    <body>
      <div id="qunit"></div>
      <div id="qunit-fixture">
          <ul>
              <li class="entry" id="post57">
                  <p>abc abc acb acb …</p>
                  <small class="extra">
                                    Postado em <a href="/2014/12/abc/57/" title="2014-12-02T20:24:17Z">02 de Dezembro de 2014</a>
                                    por <a href="/john/">Sueila Valente</a>
                             </small>
              </li>
              <li class="entry" id="post57">
                  <p>abc abc acb acb …</p>
                  <small class="extra">
                                    Postado em <a href="/2014/12/abc/57/" title="2014-12-01T20:24:17Z">01 de Dezembro de 2014</a>
                                    por <a href="/john/">Sueila Valente</a>
                             </small>
              </li>
          </ul>
      </div>
   </body>
  </html>

Assim, as mudanças no arquivo nossadata.js resultam na Listagem 11.

Listagem 11. Mudanças de organização no código JS do projeto.
var nossaData = {
      format: function(now, time){
          var date = new Date(time || ""),
              diff = (((new Date(now)).getTime() - date.getTime()) / 1000),
              day_diff = Math.floor(diff / 86400);
   
          if (isNaN(day_diff) || day_diff < 0 || day_diff >= 31) {
              return;
          }
   
          return day_diff === 0 && (
                  diff < 60 && "Agora" ||
                  diff < 120 && "1 minuto atrás" ||
                  diff < 3600 && Math.floor( diff / 60 ) + " minutos atrás" ||
                  diff < 7200 && "1 hora atrás" ||
                  diff < 86400 && Math.floor( diff / 3600 ) + " horas atrás") ||
              day_diff === 1 && "Ontem" ||
              day_diff < 7 && day_diff + " dias atrás" ||
              day_diff < 31 && Math.ceil( day_diff / 7 ) + " semanas atrás";
      },
   
      update: function(now) {
          var links = document.getElementsByTagName("a");
          for ( var i = 0; i < links.length; i++ ) {
              if (links[i].title) {
                  var date = nossaData.format(now, links[i].title);
                  if (date) {
                      links[i].innerHTML = date;
                  }
              }
          }
      }
  };

O resultado da execução você pode conferir na Figura 3.

Resultado da execução dos testes refatorados
Figura 3. Resultado da execução dos testes refatorados.

A nova função nossaData.update é um extrato do exemplo inicial, mas com o argumento now para passar a nossaData.format. O teste baseado no QUnit para essa função começa selecionando todos os elementos dentro de um elemento #qunit-fixture. Na marcação atualizada no elemento do corpo, o

... é novo. Ele contém um extrato da marcação de nosso exemplo inicial, o suficiente para escrever testes úteis. Ao colocá-lo no elemento #qunit-fixture, você não precisa se preocupar com as mudanças do DOM de um teste afetando outros testes, porque o QUnit irá reiniciar automaticamente a marcação após cada teste.

Vamos dar uma olhada no primeiro teste de nossaData.update: depois de selecionar essas âncoras, duas asserções verificam se estes têm os seus valores de texto iniciais. Posteriormente, nossaData.update é chamada, passando uma data fixa por parâmetro (a mesma que nos testes anteriores). Depois, mais duas asserções são executadas, agora verificando se a propriedade innerHTML destes elementos têm a data formatada corretamente, "2 horas atrás" e "Ontem".

YUI Library

O Yahoo! User Interface Library (YUI) é uma biblioteca open-source feita em JavaScript para a construção de aplicações web interativas ricas usando técnicas como Ajax, DHTML, e DOM scripting. O YUI inclui vários recursos do núcleo do CSS e está disponível sob a licença BSD. O desenvolvimento do projeto começou em 2005 e algumas propriedades do Yahoo!, como o My Yahoo! e a página Yahoo!, começaram a usar o YUI no verão daquele ano. Este foi liberado para uso público em fevereiro de 2006. Foi desenvolvido ativamente por uma equipe central de engenheiros do Yahoo! e é hoje um projeto amplamente aceito e reconhecido pela comunidade.

O YUI Test, um componente dentro da biblioteca YUI, é um framework de testes unitários extenso e completo. Para começar com o YUI Test, você precisa:

  1. Importar o YUI no cabeçalho da página HTML, como se segue:
  2. <script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script>
  3. No arquivo de script de teste, instanciar a função YUI. Carregar os módulos necessários, test e console, como mostrado na Listagem 12.

Listagem 12. Carregar teste e console dos módulos YUI.
YUI().use("test", "console", function (Y) {
       // Casos de teste aqui
  });

Veja na seção Links o link necessário para efetuar o download do YUI Test. Estamos usando a versão 3.18.1, a mais recente na data de escrita deste artigo.

O módulo de teste é claramente necessário para fins de teste. O módulo de console não é obrigatório, mas o exemplo que faremos vai usá-lo para imprimir os resultados. Os casos de teste vão dentro da chamada de retorno, com a instância Y global como argumento.

O YUI Test usa o construtor Y.Test.Case() para instanciar um novo caso de teste e o construtor Y.Test.Suite() para instanciar um conjunto de testes. Um conjunto de testes (ou suíte de testes), de forma semelhante ao JUnit, contém vários casos de teste. Você pode adicionar casos de teste para uma suíte de testes usando o método add().

Crie uma nova página HTML e adicione o código a seguir nela:

YUI().use("test", "console", function (Y) {

Esse código representa o repositório web que responderá aos comportamentos de alguns testes.

Em seguida, crie um novo arquivo JavaScript e adicione o conteúdo da Listagem 13. Nele podemos ver a adição de algumas funções de medição de temperatura e conversão. Apenas funções básicas para simularmos os comportamentos no YUI Test.

Listagem 13. Código JavaScript das conversões de temperatura.
function converterDeCelsiusParaFahrenheit(c){
      var f = c * (9/5) + 32;
      return f;
  }
   
  function converterDeFahrenheitParaCelsius(f){
      var c = (f - 32) * (5/9);
      return c;
  }

E por fim, crie um novo arquivo .js test_temp.js para guardar o código de teste para as mesmas funções, como mostra a Listagem 14.

Listagem 14. Suíte de testes para as funções de conversão.
module ("Conversão de Temperatura")
   
  test("conversão para F", function(){
      var actual1 = converterDeCelsiusParaFahrenheit(20);
      equal(actual1, 68, ?Valor não correto?);
         
      var actual2 = converterDeCelsiusParaFahrenheit(30);
      equal(actual2, 86, ?Valor não correto?);
  })
   
  test("conversão para C", function(){
      var actual1 = converterDeFahrenheitParaCelsius(68);
      equal(actual1, 20, ?Valor não correto?);
   
      var actual2 = converterDeFahrenheitParaCelsius(86);
      equal(actual2, 30, ?Valor não correto?);
  })

Se estiver atento, verá que o código apresentado nessa listagem está em QUnit. Isso quer dizer que a representação dele aqui se dará apenas com o objetivo de comparar os resultados e efeitos de implementação em ambos os frameworks. Não exibiremos o resultado no QUnit para não estender muito o artigo.

Agora vejamos como fica o nosso código fonte usando o YUI Test. A Listagem 15 mostra como criar uma suíte e um caso de teste para o teste em questão usando o referido framework.

Listagem 15. Suíte de testes para funções de conversão com o YUI Test.
YUI().use("test", "console", function (Y) {     
       var suite = new Y.Test.Suite("Suíte de Conversão de Temperatura");
   
       // add o caso de teste
       suite.add(new Y.Test.Case({
               name: "Conversão de Temperatura",
   
               setUp : function () {
                      this.celsius1 = 20;
                      this.celsius2 = 30;
                      
                      this.fahrenheit1 = 68;
                      this.fahrenheit2 = 86;
               },
   
               testConversaoCtoF: function () {
                      Y.Assert.areEqual(this.fahrenheit1, converterDeCelsiusParaFahrenheit(this.celsius1));
                      
                      Y.Assert.areEqual(this.fahrenheit2, converterDeCelsiusParaFahrenheit(this.celsius2));
               },
                      
               testConversaoFtoC: function () {
                      Y.Assert.areEqual(this.celsius1, converterDeFahrenheitParaCelsius(this.fahrenheit1));
                             
                      Y.Assert.areEqual(this.celsius2, converterDeFahrenheitParaCelsius(this.fahrenheit2));
               }
         }));
  });

Vejamos algumas observações sobre a listagem:

  • O método setup() está disponível. O YUI Test fornece os métodos setup() e tearDown() a nível de caso de teste e suíte de testes.
  • Os nomes de métodos começam com a palavra test e contêm assertivas.
  • O exemplo utiliza o tipo de assertiva Y.Assert.areEqual(), que é semelhante à função de equal() no QUnit.
  • O YUI Test oferece uma vasta gama de métodos para asserções, tais como:
    • Y.Assert.areSame(), que é o equivalente ao strictEqual() no QUnit.
    • Tipo de dados assertivos: Y.Assert.isArray(), Y.Assert.isBoolean(), Y.Assert.isNumber(), e assim por diante.
    • Asserções de valor especial: Y.Assert.isFalse(), Y.Assert.isNaN(), Y.Assert.isNull(), e assim por diante.

Para executar os testes no YUI Test use o objeto Y.Test.Runner. Você precisa adicionar casos de teste ou suítes de teste a esse objeto e então chamar o método run() para rodar os testes. O código a seguir mostra como fazer isso:

Y.Test.Runner.add(suite);
  Y.Test.Runner.run(); 

JSTestDriver

O JsTestDriver visa ajudar desenvolvedores JavaScript a usar boas práticas de TDD e fazer testes de unidade de escrita tão facilmente como o que já existe hoje para Java com JUnit, por exemplo. Com a ferramenta poderosa JSTestDriver (JSTD) você pode executar JavaScript na linha de comando em vários navegadores. Após baixar o JSTestDrver (ver seção Links) ele vem com um arquivo JAR que permite iniciar um servidor, capturar um ou vários navegadores, e executar testes nos navegadores. Você não precisa de um executor de HTML, como acontece com outros frameworks, mas é necessário um arquivo de configuração. A Listagem 16 mostra um exemplo de arquivo de configuração.

Listagem 16. Exemplo de arquivo de configuração – jsTestDriver.conf.
server: http://localhost:4224
  load:
    - js/src/*.js
  test:
    - js/test/*.js

O arquivo de configuração é escrito em YAML, que é um bom formato para arquivos de configuração. Ele contém informações sobre o servidor para executar, bem como a localização dos arquivos de código fonte e teste.

Para executar os testes com JSTD basta:

  1. Iniciar o servidor de teste. A partir da linha de comando, vá para a pasta onde o arquivo jsTestDriver.jar foi salvo e execute o seguinte comando:
  2. java -jar JsTestDriver-1.3.3d.jar -port 4224
    A porta especificada na Listagem 16 deve ser a mesma que a especificada no arquivo de configuração. Por padrão, o JSTD procura pelo arquivo jsTestDriver.conf no mesmo diretório onde o arquivo JAR reside.
  3. Registre um ou vários navegadores para o servidor, copiando e colando a URL http:// localhost:4224/capture nos navegadores em teste.

Teste o mesmo código fonte que você usou para os exemplos anteriores (conversão de temperaturas), mas desta vez usando a sintaxe JSTD. A Listagem 17 mostra como converter os casos de teste a partir da Listagem 14 para o QUnit e da Listagem 15 para o YUI Test.

Listagem 17. Exemplo de arquivo de configuração – jsTestDriver.conf.
TestCase("Conversão de Temperatura", {
      setUp : function () {
          this.celsius1 = 20;
          this.celsius2 = 30;
         
          this.fahrenheit1 = 68;
          this.fahrenheit2 = 86;
      },
   
      testConversaoCtoF: function () {
          assertSame(this.fahrenheit1, converterDeCelsiusParaFahrenheit(this.celsius1));
          assertSame(this.fahrenheit2, converterDeCelsiusParaFahrenheit(this.celsius2));
      },
         
      testConversaoFtoC: function () {
          assertSame(this.celsius1, converterDeFahrenheitParaCelsius(this.fahrenheit1));
          assertSame(this.celsius2, converterDeFahrenheitParaCelsius(this.fahrenheit2));
      }
  });

O código não é muito diferente da versão YUI. O JSTD usa a função TestCase() para definir um caso de teste. É possível definir pode definir os métodos de teste usando uma declaração in-line ou você pode estender o protótipo da instância TestCase. Os métodos setup() e tearDown() estão disponíveis para cada caso de teste.

Para executar os testes, basta executar o seguinte comando:

 java -jar JsTestDriver-1.3.3d.jar --tests all

A Figura 4 mostra o resultado da execução destes testes.

Resultados dos testes JSTD
Figura 4. Resultados dos testes JSTD.

O JSTD também se integra bem com o seu sistema de integração contínua para fazer parte de sua construção contínua. Ele oferece integração com IDEs como Eclipse (plug-in) ou TextMate (bundle).

Com o foco atual no lado do cliente das aplicações web, os testes unitários com código JavaScript tornaram-se essenciais. Há alguns frameworks que podem ajudar você a realizar seus objetivos, e este artigo analisou três dos principais frameworks mais populares disponíveis no mercado hoje: o QUnit, o YUI Test e o JSTestDriver.

Os frameworks que vimos aqui representam apenas uma pequena parte do universo de opções disponíveis para testes de unidade. O mais importante é que você sempre entenda os conceitos para que assim possa aplica-los na prática junto aos mesmos.

Existem inúmeras outras possibilidades de melhoria para os códigos aqui apresentados e você pode melhorá-los de diversas formas. Além disso, outros frameworks podem ser explorados dadas as suas condições. Leia mais sobre os frameworks, faça testes e mais testes até se sentir familiarizado com tais tecnologias. Com os fontes disponibilizados para download você terá um material rico para avançar seus estudos na criação de testes de unidade com JavaScript.

Confira também