O ecossistema de Testes em JavaScript em 2017

Uma visão geral sobre ferramentas, estratégias e o que escolher!

Diferentes escolhas para cada tamanho de projeto e problema!

Esse pequeno guia tem a intenção de atualizar seu raciocínio em termos de ferramentas e abordagens para testes em JavaScript. Ele combina vários tópicos abordados em artigos escritos recentemente, onde iremos discuti-los aqui e ir além, combinando com a nossa experiência.

Tabela de conteúdo

  1. Tipos de Testes
  2. Tipos de ferramentas de Testes
  3. Colocando tudo isso junto
  4. Ferramentas de testes mais usadas atualmente
  5. Escolha seu framework
  6. Testes unitários
  7. Testes de integração
  8. Testes Funcionais
  9. Contribua para esse guia você também!
  10. Conclusão
  11. Artigos sugeridos

Dê uma olhada no logo do Jest, um framework de testes criado pelo Facebook:

Slogan: Testes em JavaScript sem dor!

Como você pode ver no slogan, ele promete testes em JavaScript “sem dor”, mas, como algumas pessoas já comentaram:

Não existem esse tipo de coisa, “testes sem dor”

E realmente, Facebook tem uma excelente razão para usar esse slogan. Em geral, desenvolvedores JS não estão felizes com as maneiras disponíveis para testar websites. Testes em JS tendem a ser limitados, difíceis de implementar e lentos.

Mesmo assim, com a estratégia correta e a combinação certa de ferramentas, é possível alcançar quase uma cobertura completa de testes, bem organizados, simples e relativamente rápidos.

Vale mencionar que existem várias ótimas bibliotecas que não são mantidas, na qual eu encontrei escrevendo esse artigo, com características únicas que podem ser bem úteis em várias situações se sua empresa decidir ressuscitar e mantê-la, um exemplo é DalekJS.

Eu realmente espero criar um resumo dessas bibliotecas em um artigo futuro, mas, por hora, vamos focar no que está “quente” e sendo mantido diariamente.


Tipos de Testes

Você pode ler sobre as diferenças de testes, com mais detalhes, nesse artigo, nesse também e nesse (todos em inglês).

Em geral, os testes mais importantes são:

  • Testes Unitários (Unit Tests): Testes individuais de funções ou classes, através de mocks de dados e tendo a certeza de que os dados de saída são os esperados.
  • Testes de Integração (Integration Tests): Testando vários módulos para ter a certeza que eles funcionam juntos, da maneira esperada.
  • Testes Funcionais (Functional Tests): Testando um cenário no próprio produto (no navegador, por exemplo), independentemente da estrutura interna para garantir o comportamento esperado

Tipos de ferramentas de Testes

Ferramentas de testes podem ser divididas nas seguintes funcionalidades. Algumas disponibilizam apenas uma funcionalidade e outras uma combinação delas.

É comum combinar várias ferramentas para alcançar uma estrutura mais flexível, mesmo que apenas uma delas disponibilize tudo o que você quer.

  1. Disponibiliza um ambiente de testes (Mocha, Jasmine, Jest, Karma)
  2. Disponibiliza uma estrutura para testes (Mocha, Jasmine, Jest, Cucumber)
  3. Disponibiliza funções de assertação (Chai, Jasmine, Jest, Unexpected)
  4. Gera, mostra e observa os resultados do seus testes (Mocha, Jasmine, Jest, Karma)
  5. Gera e compara snapshots de componentes e estrutura de dados para ter certeza que mudanças de testes anteriores são intencionais (Jest, Ava)
  6. Disponibiliza mocks, spies, e stubs (Sinon, Jasmine, enzyme, Jest, testdouble)
  7. Gera relatórios de cobertura de código (Istanbul, Jest)
  8. Disponibiliza um navegador ou um “ambiente igual navegador” com controle dos seus cenários de execução (Protractor, Nightwatch, Phantom, Casper)

Vamos explicar alguns dos termos usados acima:

Estrutura para testes, é mais relacionado a organização e a intenção dos seus testes. Normalmente, usamos uma estrutura BDD, em um ambiente que suporta behavior-driven development, se parecendo com:

describe(‘calculadora’, function() {
// descreve módulos com funções “describe” aninhadas
describe(‘soma’, function() {
// especifica o comportamento esperado
it(‘adiciona 2 números', function() {
// Usa funções de asserção para testar o comportamento
})
})
})

Funções de assertação são funções que comparam o resultado com o valor esperado. Os mais famosos são os dois primeiros:

// Chai
expect(foo).to.be.a('string')
expect(foo).to.equal('bar')
// Jasmine
expect(foo).toBeString()
expect(foo).toEqual('bar')
// Chai
assert.typeOf(foo, 'string')
assert.equal(foo, 'bar')
// Unexpected
expect(foo, 'to be a', 'string')
expect(foo, 'to be', 'bar')

Dica: Veja esse artigo para saber mais sobre asserções avançadas em Jasmine.

Spies, disponibilizam informações sobre uma função que foi usada na aplicação ou criada para apenas um teste. Quantas vezes ela foi chamada, em quais casos, e por quem? Elas são especialmente úteis em testes de integração onde nós queremos ter certeza de que alguns comportamentos estão rodando em cenário interno específico. Por exemplo, quantas vezes uma função de cálculo foi chamada durante o processo?

it(‘chama o método uma única vez, com argumento 3', () => {
const spy = sinon.spy(object, 'method')
spy.withArgs(3)
object.method(3)
assert(spy.withArgs(3).calledOnce)
})

Stubbing e dubbing (assim como dublês nos filmes), subistituem funções selecionadas para ter certeza de que um comportamento no módulo selecionado ocorreu corretamente.

Se nós quisermos ter certeza de que user.isValid() sempre retorna true durante um teste, por exemplo, você pode fazer:

// Sinon
sinon.stub(user, 'isValid').returns(true);
// Jasmine
spyOn(user, 'isValid').andReturns(true);

Isso também funciona com Promises:

it(‘resolve com o nome correto', done => {
const stub = sinon.stub(User.prototype, 'fetch')
.resolves({ name: ‘Eduardo’ })
User.get()
.then(user => {
expect(user.name).toBe(‘Eduardo’)
done()
})
})

Mocks ou Fakes irão fingir certos módulos ou comportamentos para garantir que o teste retorna com dados esperados. Sinon faz isso com um servidor falso para garantir resultados rápidos e esperados:

it(‘retorna um objeto contendo todos os usuários', done => {
const server = sinon.fakeServer.create()
server.respondWith('GET', '/users', [
200,
{ 'Content-Type': 'application/json' },
'[{ "id": 1, "name": "Gwen" }, { "id": 2, "name": "John" }]'
])
Users.all()
.done(collection => {
const expectedCollection = [
{ id: 1, name: 'Gwen' },
{ id: 2, name: 'John' }
]
expect(collection.toJSON()).to.eql(expectedCollection)
done()
})
server.respond()
server.restore()
});

Testes de snapshot é quando você compara o resultado de uma estrutura de dados com outra. Por exemplo, o seguinte test, escrito em Jest, simula a renderização de um componente “Link” e salva seu resultado em um JSON.

Então, ele compara o resultado com o resultado anterior. Se algo mudou, o desenvolvedor é notificado para aceitar ou não as mudanças, caso elas sejam intencionais.

it(‘renderiza corretamente', () => {
const linkInstance = (
<Link page="http://www.facebook.com">Facebook</Link>
)
const tree = renderer.create(linkInstance).toJSON()
expect(tree).toMatchSnapshot()
})
Quase um Git Diff das mudanças!

Colocando tudo isso junto

É recomendado, se possível, usar a mesma ferramenta para todos os tipos de testes. Tais como, mesma estrutura de testes e síntaxe (item 2), funções de assertação (item 3), relatório e observação de resultados (item 4), tente usar a mesma ferramenta para eles. Algumas vezes, até para o ambiente de testes (item 1), pode fazer parte da mesma ferramenta.

Tenha certeza de que é possível rodar particularmente, em demanda, alguns tipos de testes.

  • Para testes unitários, disponibilize todos eles utilizando dados em mocks (item 6), e tenha certeza que o resultado de saída é o esperado (item 3). Também tenha certeza de gerar relatórios de cobertura de código (item 7), para saber quais arquivos estar cobertos com testes.
  • Para testes de integração, defina cenários de caso de uso entre módulos internos. Se você comparar com testes unitários, você irá spies e stubs para testar o comportamento esperado, ao invés de apenas dados em mocks e o resultado de saída (item 6). E para um navegador ou “ambiente igual navegador”, você pode testar integrações entre processos e seus resultados na UI.
  • Para testes funcionais, um navegador ou “ambiente igual navegador”, com uma API programável (item 8), podem ser usados para criar cenários reais de usuário.

Ferramentas de testes mais usadas atualmente

JSDom é uma implementação JavaScript do WHATWG DOM e padrões HTML. Em outras palavras, JSDom simula um ambiente de navegador sem precisar rodar nada ao invés de puro JS.

Nesse ambiente simulado, testes podem ser rodados bem rápido. O lado ruim é que o JSDom não simula tudo que um navegador real faz (você não pode tirar um screenshot, por exemplo), usá-lo pode limitar o alcance dos seus testes.

Vale mencionar que a comunidade JS contribui bastante para seu melhoramento.

Istanbul irá te dizer qual a quantidade do seu código que está coberto por testes. Ele irá informar cada declaração, linha, função e arquivo que está coberto, mostrando em porcentagens, para você entender melhor quais partes do seu código está faltando testes.

Phantom implementa um navegador Webkit sem GUI, ficando entre um navegador real com a velocidade e estabilidade do JSDom.

Ele é bem popular, no momento que esse artigo foi escrito. Porém, vale notar que o Google está adicionando a mesma funcionalidade, nativamente no Chrome.

E por isso, Phantom não será mais mantido oficialmente.

Karma deixa você rodar testes em um navegador, incluindo navegadores reais, Phantom, JSDom e até mesmo navegadores antigos.

Karma hospeda um servidor de testes com uma página especial para rodar seus testes. Essa página pode ser executada em vários navegadores.

Isso significa que seus testes podem ser executados remotamente usando serviços como BrowserStack.

Chai é a biblioteca mais popular de assertações.

Unexpected é uma biblioteca de assertação com uma pequena diferença em sintaxe, se comparado com Chai. Ela também é extensível para criar assertações mais avançadas, como unexpected-react. Você pode ler mais sobre nesse link (em inglês).

Sinon é uma biblioteca poderosa de spies, stubs e mocks para JavaScript, que funciona com qualquer framework de testes.

testdouble é uma nova biblioteca que é similar ao Sinon, com algumas diferenças no design, filosofia e características que podem ser úteis em vários casos. Você pode ler mais sobre, aqui, aqui e aqui (todos em inglês).

Wallaby é outra ferramenta que vale mencionar. Ela não é gratuita, mas muitos usuários recomendam comprá-la. Ela roda na sua IDE (tem suporte para todas as mais famosas), e conforme você modifica seu código, os testes serão rodados e indicados para você em tempo real (verde, o teste passou, vermelho, falhou).

Resultado dos testes em tempo real no "gutter" do seu editor!

Escolha seu framework

A primeira escolha que você deve fazer, é qual framework você irá usar e quais bibliotecas ele suporta. É recomendado usar as ferramentas que seu framework disponibiliza, até que a necessidade de uma ferramenta específica apareça. Nesse momento, é essencial que não seja difícil mudar/adicioná-la na sua pilha de ferramentas.

Algumas opiniões que recebi ao longo desse artigo:

  • Se você quiser um framework com rápida configuração, rápido e com suporte para projetos de grande porte, use Jest.
  • Se você quiser um bem flexível e que possa ser extendido com configurações, use Mocha.
  • Se você quiser simplicidade, use Ava.
  • Se você está procurando integrações de baixo nível, use tape.

Abaixo, listo algumas ferramentas proeminentes atualmente, seus prós e contras:

Jasmine é um framework de testes, disponibilizando tudo o que você precisa para seus testes. Roda um ambiente, tem estrutura, gera relatórios, asserções e ferramentas de mock.

  • Escopo global — Cria testes em escopo global por padrão, você não precisa importá-los:
// “describe” já está no escopo global
// você não precisa importar, como:
//
// const jasmine = require('jasmine')
// const describe = jasmine.describe
//
describe('calculator', function() {
...
})
  • Pronto para uso — Vem com asserções, spies, mocks que são equivalentes há bibliotecas como Sinon. Outras bibliotecas também podem ser usadas, caso você queira uma característica específica.
  • Angular — É o framework escolhido pelo projeto AngularJS

Mocha, atualmente, é a biblioteca mais utilizada. Diferente do Jasmine, ele usa bibliotecas de terceiros para asserções, mock e spies (normalmente Enzyme e Chai).

Isso significa que Mocha é um pouco mais difícil de começar, tendo que ser dividido em mais bibliotecas, mas isso o torna mais flexível.

Por exemplo, se você quiser alguma lógica especial nas suas asserções, você pode substituir Chai pela sua própria biblioteca de asserção. Isso também pode ser feito com Jasmine, mas no Mocha, é bem mais flexível (ele é feito para isso!).

  • Comunidade — Inúmeros plugins e extensões para testar cenários únicos
  • Extensibilidade — Pugins, extensões e bibliotecas como Sinon, incluem uma gama de funcionalidades que Jasmine não tem
  • Escopo global — Cria uma estrutura de teste com escopo global como padrão, mas claro, não para asserções, spies e mocks, como Jasmine — algumas pessoas, aparentemente, ficam surpresas com essa inconsistência de globais.

Jest é o framework recomendado pelo Facebook. Ele envolve Jasmine e adiciona várias funcionalidades em cima, então, tudo que mencionamos no Jasmine, se aplica aqui também.

Depois de ler vários artigos sobre Jest, é incrível ver como que, no final de 2016, todo mundo ficou impressionado com sua performance e comodidade.
  • Performance — Primeiro de tudo, Jest é considerado como o mais rápido para projetos de grande porte, com vários arquivos para serem testados, tudo isso devido a sua implementação inteligente de testes em paralelo (mais alguns links relevantes, aqui, aqui, aqui e aqui).
  • UI — Limpa e simples de usar
  • Testes de snapshot — jest-snapshot é desenvolvido e mantido pelo Facebook, apesar de poder ser utilizado por qualquer framework, sendo possível através de integrações ou usando os plugins corretos.
  • Mock de módulos melhorado — Jest permite que você faça mock de grande bibliotecas de um modo muito, muito simples, melhorando a velocidade dos seus testes.
  • Cobertura de código — Inclui, por padrão, uma poderosa e rápida ferramenta de cobertura de testes, baseada no Istanbul.
  • Suporte — Jest continua em desenvolvimento e melhoria contínua, dando grande passos em diversos projetos no final de 2016 e começo de 2017.
  • Experiência durante o desenvolvimento — Jest só atualiza os arquivos que foram modificados, rodando os seus testes de uma maneira bem rápida.

Ava é uma biblioteca de testes, minimalista, para rodar testes em paralelo.

  • Escopo global — Não cria testes em escopo global, deixando para você, um maior controle
  • Simplicidade — estrutura simples e asserções sem uma API complexa, junto de algumas funções avançadas
  • Experiência durante o desenvolvimento — Ava só atualiza os arquivos que foram modificados, rodando seus testes de uma maneira mais rápida.
  • Testes de snapshot — disponibiliza suporte usando jest-snapshot por baixo dos panos.

Tape é o mais simples de todos eles. É apenas um arquivo JS que você roda com NodeJS e com uma API bem pequena e direta ao ponto.

  • Simplicidade — Estrutura minimalista e asserções sem uma API complexa. Até mais simples que Ava.
  • Escopo global — Não cria testes em escopo global, deixando para você, um maior controle
  • Não compartilha estado entre testes — Tape desencoraja o uso de funções como “beforeEach”, para garantir a modularidade e máximo controle sobre o ciclo dos seus testes.
  • Não é necessário uma CLI — Tape roda simplesmente em qualquer lugar que JS roda.

Testes unitários

Eles podem ser criados para todas as funções e módulos do seu sistema. Use uma ferramenta como Istanbul para ter certeza que cada módulo na sua base de código está coberto por testes.

Esses testes estão verificando módulos separadamente, é melhor usar NodeJS para rodar esse tipo de testes e não um navegador (como Karma faz). Rodar JS em navegador é bem mais lento que em NodeJS.

Testes de integração

Crie uma lista de importantes rotinas internas do seu sistema. Implemente um por um, utilizando dados em mocks. Podendo até considerar testes de snapshot.

Teste de snapshot podem ser um bom substituto para tradicionais testes de integração de UI. Ao invés de testar partes da UI em um certo processo, você simplesmente pode tirar snapshots dessas partes.

Considere usar JSDom ou Karma, para rodar seus testes em um ambiente de navegador real.

Testes Funcionais

O número de ferramentas, permanentes, para esse tipo de teste, é bem limitado, e suas implementações são bem diferente uma das outras. Eu recomendo você tentar implementar algumas delas antes de tomar uma decisão.

Algumas opiniões que colhetei ao longo desse artigo:

  • Se você quiser algo para começar já, com rápida e simples configuração, sendo possível testar fácilmente em vários ambientes, use TestCafe
  • Se você quiser o que é mais utilizado e ter o máximo de suporte da comunidade, e ter que escrever testes utilizando outras linguagens que não JS, use Selenium
  • Se sua aplicação não tem interações complexas e gráficos, por exemplo, e você quer testar um sistema cheio de formulários e navegação, com um navegador sem GUI, use Casper

SeleniumHQ, mais conhecido como Selenium, automatiza testes em navegador para simular o comportamento do usuário. Não é especificamente para testes, podendo controlar o navegador de várias maneiras, expondo um servidor que simula o comportamento do usuário em um navegador, através de uma API.

Selenium pode ser controlar de várias maneiras, usando várias linguagens de programação e com algumas ferramentas que nem precisam de programação!

No entanto, para nossa necessidade, o servidor do Selenium é controlado pelo Selenium WebDriver. Esse servidor se comunica com a camada entre o NodeJS e o servidor que opera o navegador.

Algo como:

NodeJS <=> WebDriver <=> Selenium Server <=> FF/Chrome/IE/Safari

O WebDriver pode ser importado no seu framework de teste e os testes podem ser escritos como parte dele:

describe(‘formulário de login', () => {
before(() => {
return driver.navigate().to('http://caminho.do.seu.site/')
})
it('autocomplete do campo nome’, () => {
driver.findElement(By.css('.autocomplete'))
.sendKeys(‘Eduardo’)
driver.wait(until.elementLocated(By.css('.suggestion')))
driver.findElement(By.css('.suggestion')).click()
return driver.findElement(By.css('.autocomplete'))
.getAttribute('value')
.then(inputValue => {
expect(inputValue).to.equal(‘Eduardo Rabelo’)
})
})
after(() => {
return driver.quit()
})
})

Só o WebDriver já é o suficiente para você e algumas pessoas poderem criar testes. Sem a necessidade de plugins ou outra bibliotecas, apenas do modo que ele é.

Existem várias bibliotecas que podem extender, alterar ou envolver o WebDriver.

A idéia de envolver o WebDriver em outra biblioteca pode ser redundante, deixando a depuração difícil e podendo se afastar do código fonte original, já que ele está em constante desenvolvimento (pelo menos em 2017).

Ainda sim, muita gente prefere usar essas abstrações. Vamos olhar em algumas delas:

Protactor é uma biblioteca que envolve o Selenium e adiciona uma sintaxe melhorada e definições especiais para o Angular.

  • Angular — definições especiais, mesmo assim, podemos alcançar o mesmo resultado em outros frameworks
  • Relatório de erros — Um bom mecanismo para
  • Mobile — Não tem suporte para testes automatizados em mobile
  • Suporte — suporte para TypeScript já está disponível e a biblioteca opera e é mantida pelo enorme time do Angular

WebDriverIO tem sua própria implementação do Selenium WebDriver.

  • Sintaxe — bem fácil e de leitura agradável
  • Flexível — Bem simples e agnóstico meio de ser usado para testes, como uma biblioteca extensível
  • Comunidade — Tem um bom suporte e desenvolvedores entusiasmados, que criam uma variedade de plugins e extensões

Nightwatch tem sua própria implementação do Selenium WebDriver. E disponibiliza seu próprio framework de testes, com um servidor de testes, assertações e outras ferramentas.

  • Framework — Pode ser usado com outros frameworks também, podendo ser especialmente útil, caso você queira rodar testes funcionas que não fazem parte desse outro framework
  • Sintaxe — Parece o mais fácil e o mais legível
  • Suporte — Não suporta TypeScript, e em geral, essa biblioteca me parece ser a que tem menor suporte/comunidade

Casper é escrito em cima do Phantom e Slimer (o mesmo que o Phantom mas utilizando Firefox’s Gecko), disponibilizando navegação, scripts, utilitários para testes que abstraem a complicada, assíncrona criação de testes em Phantom e Slimer.

Casper e outros navegadores sem GUI, disponibilizam uma rápida, mas menos estável, maneira de rodar testes funcionais em navegadores.

TestCafe é uma ótima alternativa para ferramentas baseadas no Selenium

Em Outubro de 2016, eles abriram o código fonte do TestCafe, passando a ser um framework JS open-source. Ainda existe uma versão paga, que oferece ferramentas não-JS, como a habilidade de gravar testes e suporte ao consumidor.

Isso é importante porque, muitos artigos desatualizados, falam que essa biblioteca é fechada e usam isso como desvantagem.

TestCafe se injeta no ambiente do navegador como um script JS, ao invés de anexar plugins, como o Selenium faz. Isso permite TestCafe ser rodado em qualquer navegador, includingo aparelhos móveis. Em Selenium, você precisa instalar uma gama especial de plugins para cada aparelho e navegador.

TestCafe é uma ferramenta nova e mais orientada a JS. Tem uma funcionalidade bem útil de reportar erros, indicando a linha que falhou, seletores bem úteis e algumas outras funções bem interessantes, vale a pena conferir!

Cucumber é outro framework bem útil para testes funcionais. Ele organiza os testes automatizados de uma maneira, ligeiramente, diferente.

Cucumber nos ajuda a escrever testes em BDD, dividindo eles entre a equipe de negócios que escreve os requerimentos usando uma sintaxe Gherkin e os programadores que escrevem os testes de acordo com esse arquivo gerado pelo líder de negócio. Testes podem ser escritos usando uma variedade de linguagens, incluindo JS, que é o vamos usar:

Primeiro, o arquivo com sintaxe Gherkin, vamos chamar de features/like-article.feature:

Feature: A reader can share an article to social networks
As a reader
I want to share articles
So that I can notify my friends about an article I liked
Scenario: An article was opened
Given I'm inside and article
When I share the article
Then the article should change to a "shared" state

Em seguida, nossos testes em features/stepdefinitions/like-article.steps.js:

module.exports = function() {
this.Given(/^I'm inside and article$/, function(callback) {
// testes funcionais aqui
})
this.When(/^I share the article$/, function(callback) {
// testes funcionais aqui
})

this.Then(/^the article should change to a "shared" state$/, function(callback) {
// testes funcionais aqui
})
}

A sintaxe Gherkin suporta até 60 línguas, o trecho acima não foi traduzido, mas você pode verificar a documentação para aprender como escrever em pt.

Esse tipo de separação pode ajudar diferentes equipes na sua empresa a colaborar mais com a base de código, no final, essa ferramenta pode ser útil para você!


Contribua para esse guia você também!

Se você quiser que eu adicionei alguma outra biblioteca, mude algo ou altere algum trecho desse guia, é só comentar abaixo! Ficarei feliz em receber opiniões!

Eu quero que esse guia seja o mais preciso, completo e útil possível, em português.


Conclusão

Nesse pequeno guia, nós conhecemos as ferramentas mais “quentes” do momento, no quesito de estratégias de testes e ferramentas da comunidade JS. Eu espero que ele te ajude a tomar uma decisão e facilite suas escolhas para a sua aplicação.

Lembre-se, existem diversas outras ferramentas e estratégias que esse guia não cobre. Algumas são relacionadas com outras ferramentas ou estratégias completamente diferentes.

No final, a melhor decisão a ser tomada hoje, para a arquitetura da sua aplicação, é conhecer as ferramentas desenvolvidas e ativas na comunidade, atualmente, e entender os problemas que elas resolvem. Combinando isso com sua própria experiência e conhecimento das características e casos especiais da sua aplicação.

Ah!…E claro, escrever, reescrever, reescrever de novo e de novo, testando diferentes soluções e ferramentas!

Feliz suíte de testes! Muito obrigado!