Os testes automatizados serão usados em tarefas futuras e também são amplamente utilizados em projetos reais.
Quando escrevemos uma função, geralmente podemos imaginar o que ela deveria fazer: quais parâmetros fornecem quais resultados.
Durante o desenvolvimento, podemos verificar a função executando-a e comparando o resultado com o esperado. Por exemplo, podemos fazer isso no console.
Se algo estiver errado – então corrigimos o código, executamos novamente, verificamos o resultado – e assim por diante até que funcione.
Mas essas “repetições” manuais são imperfeitas.
Ao testar um código por meio de execuções manuais, é fácil perder alguma coisa.
Por exemplo, estamos criando uma função f
. Escreveu algum código, testando: f(1)
funciona, mas f(2)
não funciona. Corrigimos o código e agora f(2)
funciona. Parece completo? Mas esquecemos de testar novamente f(1)
. Isso pode levar a um erro.
Isso é muito típico. Quando desenvolvemos algo, temos em mente muitos casos de uso possíveis. Mas é difícil esperar que um programador verifique todos eles manualmente após cada alteração. Assim fica fácil consertar uma coisa e quebrar outra.
Teste automatizado significa que os testes são escritos separadamente, além do código. Eles executam nossas funções de diversas maneiras e comparam os resultados com os esperados.
Vamos começar com uma técnica chamada Behavior Driven Development ou, resumidamente, BDD.
BDD é três coisas em uma: testes E documentação E exemplos.
Para entender o BDD, examinaremos um caso prático de desenvolvimento.
Digamos que queremos fazer uma função pow(x, n)
que eleva x
a uma potência inteira n
. Assumimos que n≥0
.
Essa tarefa é apenas um exemplo: existe o operador **
em JavaScript que pode fazer isso, mas aqui nos concentramos no fluxo de desenvolvimento que também pode ser aplicado a tarefas mais complexas.
Antes de criar o código do pow
, podemos imaginar o que a função deve fazer e descrevê-la.
Tal descrição é chamada de especificação ou, resumidamente, especificação, e contém descrições de casos de uso juntamente com testes para eles, como este:
descrever("pow", function() { it("eleva à enésima potência", function() { assert.equal(pow(2, 3), 8); }); });
Uma especificação tem três blocos de construção principais que você pode ver acima:
describe("title", function() { ... })
Que funcionalidade estamos descrevendo? No nosso caso estamos descrevendo a função pow
. Usado para agrupar “trabalhadores” – it
blocos.
it("use case description", function() { ... })
No it
descrevemos de forma legível o caso de uso específico, e o segundo argumento é uma função que o testa.
assert.equal(value1, value2)
O código dentro it
bloco, se a implementação estiver correta, deverá ser executado sem erros.
As funções assert.*
são usadas para verificar se pow
funciona conforme o esperado. Aqui estamos usando um deles – assert.equal
, ele compara argumentos e gera um erro se eles não forem iguais. Aqui verifica se o resultado de pow(2, 3)
é igual a 8
. Existem outros tipos de comparações e verificações, que adicionaremos mais tarde.
A especificação pode ser executada e executará o teste especificado em it
bloco. Veremos isso mais tarde.
O fluxo de desenvolvimento geralmente é assim:
Uma especificação inicial é escrita, com testes para as funcionalidades mais básicas.
Uma implementação inicial é criada.
Para verificar se funciona, executamos o framework de testes Mocha (mais detalhes em breve) que executa a especificação. Enquanto a funcionalidade não estiver completa, erros serão exibidos. Fazemos correções até que tudo funcione.
Agora temos uma implementação inicial funcional com testes.
Adicionamos mais casos de uso às especificações, provavelmente ainda não suportados pelas implementações. Os testes começam a falhar.
Vá para 3, atualize a implementação até que os testes não apresentem erros.
Repita as etapas 3 a 6 até que a funcionalidade esteja pronta.
Portanto, o desenvolvimento é iterativo . Escrevemos as especificações, implementamos, garantimos que os testes sejam aprovados, depois escrevemos mais testes, garantimos que funcionam, etc. No final, temos uma implementação funcional e testes para ela.
Vejamos esse fluxo de desenvolvimento em nosso caso prático.
O primeiro passo já está completo: temos uma especificação inicial para pow
. Agora, antes de fazer a implementação, vamos usar algumas bibliotecas JavaScript para executar os testes, só para ver se estão funcionando (todas irão falhar).
Aqui no tutorial usaremos as seguintes bibliotecas JavaScript para testes:
Mocha – a estrutura principal: fornece funções de teste comuns, incluindo describe
e it
e a função principal que executa testes.
Chai – a biblioteca com muitas afirmações. Permite usar muitas asserções diferentes, por enquanto precisamos apenas assert.equal
.
Sinon – uma biblioteca para espionar funções, emular funções integradas e muito mais, precisaremos dela muito mais tarde.
Essas bibliotecas são adequadas para testes no navegador e no servidor. Aqui consideraremos a variante do navegador.
A página HTML completa com estas estruturas e especificações pow
:
<!DOCTYPEhtml> <html> <cabeça> <!-- adicione mocha css, para mostrar os resultados --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- adicionar código da estrutura mocha --> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <roteiro> mocha.setup('bdd'); //configuração mínima </script> <!-- adicionar chai --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <roteiro> //chai tem muitas coisas, vamos fazer assert global deixe afirmar = chai.assert; </script> </head> <corpo> <roteiro> função pow(x, n) { /* o código da função deve ser escrito, vazio agora */ } </script> <!-- o script com testes (descreva, ele...) --> <script src="test.js"></script> <!-- o elemento com id="mocha" conterá os resultados do teste --> <div id="mocha"></div> <!-- execute testes! --> <roteiro> mocha.run(); </script> </body> </html>
A página pode ser dividida em cinco partes:
O <head>
– adiciona bibliotecas e estilos de terceiros para testes.
O <script>
com a função a ser testada, no nosso caso – com o código para pow
.
Os testes – no nosso caso, um script externo test.js
que describe("pow", ...)
acima.
O elemento HTML <div id="mocha">
será usado pelo Mocha para gerar resultados.
Os testes são iniciados pelo comando mocha.run()
.
O resultado:
A partir de agora, o teste falha, há um erro. Isso é lógico: temos um código de função vazio em pow
, então pow(2,3)
retorna undefined
em vez de 8
.
Para o futuro, observemos que existem mais executores de testes de alto nível, como karma e outros, que facilitam a execução automática de muitos testes diferentes.
Vamos fazer uma implementação simples de pow
, para os testes passarem:
função pow(x, n) { retornar 8; // :) nós trapaceamos! }
Uau, agora funciona!
O que fizemos é definitivamente uma trapaça. A função não funciona: uma tentativa de calcular pow(3,4)
daria um resultado incorreto, mas os testes seriam aprovados.
…Mas a situação é bem típica, acontece na prática. Os testes passam, mas a função funciona errado. Nossa especificação é imperfeita. Precisamos adicionar mais casos de uso a ele.
Vamos adicionar mais um teste para verificar se pow(3, 4) = 81
.
Podemos selecionar uma das duas maneiras de organizar o teste aqui:
A primeira variante – adicione mais uma assert
ao it
:
descreva("pow", function() { it("eleva à enésima potência", function() { assert.equal(pow(2, 3), 8); assert.equal(pow(3, 4), 81); }); });
A segunda – faça dois testes:
descreva("pow", function() { it("2 elevado a 3 é 8", function() { assert.equal(pow(2, 3), 8); }); it("3 elevado a 4 é 81", function() { assert.equal(pow(3, 4), 81); }); });
A principal diferença é que quando assert
dispara um erro, o bloco it
termina imediatamente. Portanto, na primeira variante, se a primeira assert
falhar, nunca veremos o resultado da segunda assert
.
Fazer os testes separados é útil para obter mais informações sobre o que está acontecendo, então a segunda variante é melhor.
E além disso, há mais uma regra que é bom seguir.
Um teste verifica uma coisa.
Se olharmos para o teste e vermos duas verificações independentes nele, é melhor dividi-lo em duas mais simples.
Então, vamos continuar com a segunda variante.
O resultado:
Como poderíamos esperar, o segundo teste falhou. Claro, nossa função sempre retorna 8
, enquanto a assert
espera 81
.
Vamos escrever algo mais real para os testes passarem:
função pow(x, n) { deixe resultado = 1; for (seja i = 0; i < n; i++) { resultado *= x; } resultado de retorno; }
Para ter certeza de que a função funciona bem, vamos testá-la para obter mais valores. Em vez de it
blocos manualmente, podemos gerá-los em for
:
descrever("pow", function() { function makeTest(x) { deixe esperado = x * x * x; it(`${x} na potência 3 é ${esperado}`, function() { assert.equal(pow(x, 3), esperado); }); } para (seja x = 1; x <= 5; x++) { makeTest(x); } });
O resultado:
Vamos adicionar ainda mais testes. Mas antes disso, vamos observar que as funções auxiliares makeTest
e for
devem ser agrupadas. Não precisaremos makeTest
em outros testes, ele é necessário apenas em for
: sua tarefa comum é verificar como pow
aumenta até a potência dada.
O agrupamento é feito com uma describe
aninhada:
descreva("pow", function() { descreva("eleva x à potência 3", function() { function makeTest(x) { deixe esperado = x * x * x; it(`${x} na potência 3 é ${esperado}`, function() { assert.equal(pow(x, 3), esperado); }); } para (seja x = 1; x <= 5; x++) { makeTest(x); } }); // ... mais testes a seguir aqui, ambos descrevem e podem ser adicionados });
A describe
aninhada define um novo “subgrupo” de testes. Na saída podemos ver o recuo intitulado:
No futuro, podemos it
mais e describe
no nível superior com funções auxiliares próprias, eles não verão makeTest
.
before/after
e beforeEach/afterEach
Podemos configurar funções before/after
que são executadas antes/depois da execução dos testes, e também funções beforeEach/afterEach
que são executadas antes/depois de cada it
.
Por exemplo:
descrever("teste", função() { before(() => alert("Teste iniciado – antes de todos os testes")); after(() => alert("Teste finalizado – depois de todos os testes")); beforeEach(() => alert("Antes de um teste – insira um teste")); afterEach(() => alert("Depois de um teste – sair de um teste")); it('teste 1', () => alerta(1)); it('teste 2', () => alerta(2)); });
A sequência de execução será:
Teste iniciado – antes de todos os testes (antes) Antes de um teste – insira um teste (beforeEach) 1 Depois de um teste – sair de um teste (afterEach) Antes de um teste – insira um teste (beforeEach) 2 Depois de um teste – sair de um teste (afterEach) Teste concluído – depois de todos os testes (depois)
Abra o exemplo na sandbox.
Normalmente, beforeEach/afterEach
e before/after
são usados para realizar inicialização, zerar contadores ou fazer outra coisa entre os testes (ou grupos de testes).
A funcionalidade básica do pow
está completa. A primeira iteração do desenvolvimento está concluída. Quando terminarmos de comemorar e beber champanhe – vamos melhorar.
Como foi dito, a função pow(x, n)
destina-se a trabalhar com valores inteiros positivos n
.
Para indicar um erro matemático, as funções JavaScript geralmente retornam NaN
. Vamos fazer o mesmo para valores inválidos de n
.
Vamos primeiro adicionar o comportamento à especificação(!):
descreva("pow", function() { // ... it("para n negativo o resultado é NaN", function() { assert.isNaN(pow(2, -1)); }); it("para n não inteiro o resultado é NaN", function() { assert.isNaN(pow(2, 1.5)); }); });
O resultado com novos testes:
Os testes recém-adicionados falham porque nossa implementação não os suporta. É assim que o BDD é feito: primeiro escrevemos testes que falham e depois fazemos uma implementação para eles.
Outras afirmações
Observe a afirmação assert.isNaN
: ela verifica NaN
.
Existem outras afirmações em Chai também, por exemplo:
assert.equal(value1, value2)
– verifica a igualdade value1 == value2
.
assert.strictEqual(value1, value2)
– verifica a igualdade estrita value1 === value2
.
assert.notEqual
, assert.notStrictEqual
– verificações inversas às acima.
assert.isTrue(value)
– verifica se value === true
assert.isFalse(value)
– verifica esse value === false
… a lista completa está nos documentos
Portanto, devemos adicionar algumas linhas ao pow
:
função pow(x, n) { se (n < 0) retorne NaN; if (Math.round(n) != n) retornar NaN; deixe resultado = 1; for (seja i = 0; i < n; i++) { resultado *=x; } resultado de retorno; }
Agora funciona, todos os testes passam:
Abra o exemplo final completo na sandbox.
No BDD, a especificação vem primeiro, seguida pela implementação. No final temos a especificação e o código.
A especificação pode ser usada de três maneiras:
Como Testes – garantem que o código funciona corretamente.
Conforme Docs – os títulos describe
e it
o que a função faz.
Como exemplos – os testes são, na verdade, exemplos práticos que mostram como uma função pode ser usada.
Com a especificação, podemos melhorar, alterar e até reescrever a função com segurança do zero e garantir que ela ainda funcione corretamente.
Isso é especialmente importante em grandes projetos quando uma função é usada em muitos lugares. Quando alteramos essa função, simplesmente não há como verificar manualmente se todos os locais que a utilizam ainda funcionam corretamente.
Sem testes, as pessoas têm duas maneiras:
Para realizar a mudança, não importa o quê. E então nossos usuários encontram bugs, pois provavelmente não conseguimos verificar algo manualmente.
Ou, se a punição para os erros for severa, como não há testes, as pessoas ficam com medo de modificar tais funções, e aí o código fica desatualizado, ninguém quer entrar nele. Não é bom para o desenvolvimento.
Os testes automáticos ajudam a evitar esses problemas!
Se o projeto estiver coberto de testes, esse problema simplesmente não existe. Após qualquer alteração, podemos executar testes e ver diversas verificações feitas em questão de segundos.
Além disso, um código bem testado possui uma arquitetura melhor.
Naturalmente, isso ocorre porque o código testado automaticamente é mais fácil de modificar e melhorar. Mas também há outro motivo.
Para escrever testes, o código deve ser organizado de tal forma que cada função tenha uma tarefa claramente descrita, entradas e saídas bem definidas. Isso significa uma boa arquitetura desde o início.
Na vida real isso às vezes não é tão fácil. Às vezes é difícil escrever uma especificação antes do código real, porque ainda não está claro como ela deve se comportar. Mas, em geral, escrever testes torna o desenvolvimento mais rápido e estável.
Posteriormente no tutorial, você encontrará muitas tarefas com testes integrados. Então você verá mais exemplos práticos.
Escrever testes requer um bom conhecimento de JavaScript. Mas estamos apenas começando a aprender. Então, para esclarecer tudo, a partir de agora você não é obrigado a escrever testes, mas você já deve ser capaz de lê-los mesmo que sejam um pouco mais complexos do que neste capítulo.
importância: 5
O que há de errado no teste de pow
abaixo?
it("Eleva x à potência n", function() { seja x = 5; deixe resultado = x; assert.equal(pow(x, 1), resultado); resultado *=x; assert.equal(pow(x, 2), resultado); resultado *=x; assert.equal(pow(x, 3), resultado); });
PS Sintaticamente o teste está correto e passa.
O teste demonstra uma das tentações que um desenvolvedor encontra ao escrever testes.
O que temos aqui são na verdade 3 testes, mas dispostos como uma única função com 3 afirmações.
Às vezes é mais fácil escrever dessa forma, mas se ocorrer um erro, é muito menos óbvio o que deu errado.
Se ocorrer um erro no meio de um fluxo de execução complexo, teremos que descobrir os dados nesse ponto. Na verdade, teremos que depurar o test .
Seria muito melhor dividir o teste em vários it
com entradas e saídas claramente escritas.
Assim:
descreva("Eleva x à potência n", function() { it("5 elevado a 1 é igual a 5", function() { assert.equal(pow(5, 1), 5); }); it("5 elevado a 2 é igual a 25", function() { assert.equal(pow(5, 2), 25); }); it("5 elevado a 3 é igual a 125", function() { assert.equal(pow(5, 3), 125); }); });
Substituímos o it
único por describe
e um grupo de blocos it
. Agora, se algo falhar, veríamos claramente quais eram os dados.
Também podemos isolar um único teste e executá-lo em modo autônomo escrevendo it.only
em vez it
:
descreva("Eleva x à potência n", function() { it("5 elevado a 1 é igual a 5", function() { assert.equal(pow(5, 1), 5); }); //Mocha executará apenas este bloco it.only("5 elevado a 2 é igual a 25", function() { assert.equal(pow(5, 2), 25); }); it("5 elevado a 3 é igual a 125", function() { assert.equal(pow(5, 3), 125); }); });