Go to English Blog

Escrevendo testes no JavaScript com Jasmine

Leia em 3 minutos

Em um dos artigos que escrevi recentemente mostrei como você poderia escrever um código em uma abordagem mais modular. Agora vamos ver como podemos escrever os testes para aquela implementação.

Vamos relembrar como é o nosso HTML.

<div class="postbox">
  <textarea placeholder="Escreva sua resposta..."></textarea>
  <button class="reply-button">Enviar esta resposta</button>
</div>

E nossa implementação de JavaScript final ficou assim:

Module("HOWTO.Postbox", function(Postbox){
  Postbox.fn.initialize = function(container) {
    this.container = container;
    this.textarea = container.find(".pb-textarea");
    this.button = container.find(".pb-button");

    this.addEventListeners();
  };

  Postbox.fn.addEventListeners = function() {
    this.textarea
      .on("focus", this.onTextareaFocus.bind(this))
      .on("keyup", this.onTextareaKeyUp.bind(this))
      .on("blur", this.onTextareaBlur.bind(this))
    ;
  };

  Postbox.fn.onTextareaFocus = function(event) {
    this.container
      .addClass("did-focus")
      .removeClass("is-contracted")
    ;
  };

  Postbox.fn.onTextareaKeyUp = function(event) {
    var lines = event.target.value.split(/\r?\n/);

    if (lines.length >= 5) {
      this.container.addClass("is-expanded");
    } else {
      this.container.removeClass("is-expanded");
    }
  };

  Postbox.fn.onTextareaBlur = function(event) {
    if (!event.target.value) {
      this.container.addClass("is-contracted");
    }
  };
});

Existe um processo chamado de Test-Driven Development (TDD), que utiliza um ciclo de desenvolvimento que começa pelos testes. Infelizmente, como nosso código já está escrito, não podemos utilizá-lo. Mas isso não impede que nosso código seja testado ou, que ele seja bem escrito. Embora TDD encorage um design mais simples, ele não é requisito.

Configurando o Jasmine

O Jasmine é a biblioteca que iremos usar nesse exemplo. Ela pode ser usada através do Ruby ou em modo standalone, que é o que vamos usar.

Faça o download da última versão disponível no site do projeto. Em vez de usarmos a estrutura sugerida pelo Jasmine, vamos adaptá-la para nosso projeto. Se quiser, faça o download da estrutura que vou utilizar. Basicamente movo tudo o que é do Jasmine para dentro do diretório lib.

Escrevendo nosso primeiro teste

Para escrever nossos testes, primeiro vamos lembrar das características do Postbox.

Vamos começar pelo nosso primeiro teste. Para isso, vamos criar um novo arquivo chamado spec/howto/postbox.spec.js. Nele iremos definir a estrutura básica de um grupo de testes com a função describe com um grupo que irá configurar nossas dependências para os testes.

describe("HOWTO.Postbox", function() {
  var postbox, textarea, container;

  beforeEach(function() {
    textarea = $("<textarea/>", {class: "pb-textarea"});
    container = $("<div/>")
      .append(textarea)
      .append(container)
      .appendTo("#sample")
    ;
    postbox = HOWTO.Postbox(container);
  });
});

A função beforeEach é executada antes de cada teste e serve muito bem para preparar a estrutura que precisamos para o teste. Como é algo muito simples, criei na mão, mas você também poderia usar alguma biblioteca que implemente o carregamento de fixtures (nome dado a esses arquivos que contém dados para testes) com AJAX, por exemplo.

Algumas verificações exigem que o elemento exista no DOM, como foco em elementos ou verificações de visibilidade.

Agora vamos adicionar nosso primeiro teste. Ele irá garantir que a classe did-focus é adicionada ao container quando o campo receber o foco.

describe("HOWTO.Postbox", function() {
  var postbox, textarea, container;

  beforeEach(function() {
    textarea = $("<textarea/>", {class: "pb-textarea"});
    container = $("<div/>")
      .append(textarea)
      .append(container)
      .appendTo("#sample")
    ;

    postbox = HOWTO.Postbox(container);
  });

  it("sets the container class on focus", function(){
    textarea.trigger("focus");
    expect(container.is(".did-focus")).toBeTruthy();
  });
});

Cada um dos testes são representados pela função it. Este função pode ter uma ou mais asserções, nome dado aos testes que adicionamos para saber se o código está se comportando como esperamos. Isso é feito com a função expect, que recebe um objeto que receberá a asserção. Finalmente, usamos um matcher para fazer a verificação.

Se tentarmos executar o arquivo spec_runner.html, perceberá que nada irá acontecer. Como estamos usando a versão standalone que é estática, temos que adicionar os arquivos de teste manualmente.

<!doctype html>
<html>
<head>
  <title>Jasmine Spec Runner</title>

  <link rel="stylesheet" href="lib/jasmine-1.3.1/jasmine.css">
  <script src="lib/jasmine-1.3.1/jasmine.js"></script>
  <script src="lib/jasmine-1.3.1/jasmine-html.js"></script>

  <!-- include source files here -->
  <script src="../vendor/jquery.js"></script>
  <script src="../vendor/module.js"></script>
  <script src="../howto/postbox.js"></script>

  <!-- include spec files here -->
  <script src="spec_helper.js"></script>
  <script src="howto/postbox.spec.js"></script>

  <!-- run all specs -->
  <script src="lib/runner.js"></script>
</head>

<body>
  <div id="sample"></div>
</body>
</html>

Abra o arquivo spec/spec_runner.html mais uma vez. Se tudo deu certo, você verá uma página desse tipo.

Test Runner do Jasmine

Experimente, por exemplo, comentar o trecho de código que adiciona a classe did-focus e você verá que o teste irá falhar.

Teste do Jasmine com falha

Hora de verificar se a classe is-expanded é adicionada quando o textarea tem mais de cinco linhas.

it("expands postbox when have more than 4 lines", function() {
  textarea.val(Array(5).join("\r\n"));
  textarea.trigger("keyup");

  expect(container.is(".is-expanded")).toBeTruthy();
});

Sempre que sua função possuir um if, como é o caso da função HOWTO.PostBox.fn.onTextareaKeyUp, escreva outro teste para garantir as demais condições. Por isso é importante ter funções simples. Quanto mais simples ela for, mais fácil será escrever o seu teste.

it("detects when postbox have less than 5 lines", function() {
  textarea.val(Array(5).join("\r\n"));
  textarea.trigger("keyup");

  textarea.val(Array(4).join("\r\n"));
  textarea.trigger("keyup");

  expect(container.is(".is-expanded")).toBeFalsy();
});

Finalmente, vamos adicionar mais dois testes para lidar com a saída do textarea.

it("sets class when leaving box without content", function() {
  textarea.trigger("blur");
  expect(container.is(".is-contracted")).toBeTruthy();
});

it("sets no class when leaving box with content", function() {
  textarea.val("Sample");
  textarea.trigger("blur");
  expect(container.is(".is-contracted")).toBeFalsy();
});

Pronto! Nosso componente está devidamente testado. E nem foi tão difícil assim, foi? Se quiser, faça o download deste exemplo.

Tudo verde!

Finalizando

A verdade é que escrever testes exige prática. No começo é mais difícil, porque você não conhece a API da biblioteca de testes. Além disso, existem técnicas diferentes de teste que são muito úteis como mocking e stubbing, e que eu não mostrei aqui, mas que devo mostrar em outros artigos.