No outro dia, farei um relatório interno no qual falarei aos nossos desenvolvedores sobre os erros desagradáveis que podem ocorrer ao escrever testes de unidade. Os erros mais desagradáveis do meu ponto de vista são quando os testes são aprovados, mas ao mesmo tempo o fazem de maneira tão incorreta que seria melhor não passar. E eu decidi compartilhar exemplos de tais erros com todos. Certamente mais alguma coisa para contar dessa área. Exemplos são escritos para Node.JS e Mocha, mas em geral esses erros são verdadeiros para qualquer outro ecossistema.
Para torná-lo mais interessante, alguns deles são estruturados na forma de um código de problema e um spoiler, abrindo o que, você verá qual era o problema. Portanto, recomendo que você primeiro analise o código, encontre um erro nele e abra o spoiler. Nenhuma solução para os problemas será indicada - proponho pensar por nós mesmos. Só porque sou preguiçosa. A ordem da lista não faz muito sentido - é simplesmente uma sequência na qual lembrei de todos os tipos de problemas reais que nos levaram a lágrimas de sangue. Certamente muitas coisas parecerão óbvias para você - mas mesmo desenvolvedores experientes podem escrever acidentalmente esse código.
Então vamos lá.
0. Falta de testes
Curiosamente, muitos ainda acreditam que a escrita de testes diminui a velocidade de desenvolvimento. Obviamente, é óbvio que é preciso gastar mais tempo escrevendo testes e escrevendo códigos que podem ser testados. Mas após a depuração e a regressão, você precisa gastar muito mais tempo ...
1. A falta de execução de testes
Se você tem testes que não executa, ou executa de tempos em tempos, é como a ausência de testes. E é ainda pior - você tem um código de teste desatualizado e uma falsa sensação de segurança. Os testes devem ser executados pelo menos nos processos de IC ao enviar o código para uma ramificação. E melhor - localmente antes do empurrão. Em seguida, o desenvolvedor não precisará retornar à compilação em alguns dias, o que, como se viu, não passou.
2. Falta de cobertura
Se você ainda não sabe qual é a cobertura nos testes, é hora de ler agora. Pelo menos
Wikipedia . Caso contrário, as chances são boas de que seu teste verifique 10% do código que você acha que verifica. Mais cedo ou mais tarde você definitivamente vai pisar nela. Obviamente, mesmo 100% de cobertura do código não garante sua completa correção de forma alguma - mas isso é muito melhor do que a falta de cobertura, pois mostrará muito mais erros em potencial. Não é de admirar que as versões mais recentes do Node.JS possuam ferramentas internas para lê-lo. Em geral, o tópico da cobertura é profundo e extremamente holístico, mas não vou me aprofundar muito nisso - quero falar um pouco sobre isso.
3)
const {assert} = require('chai'); const Promise = require('bluebird'); const sinon = require('sinon'); class MightyLibrary { static someLongFunction() { return Promise.resolve(1);
O que há de errado aquiTempos limite em testes de unidade.
Aqui eles queriam verificar se o tempo limite para uma operação longa realmente funciona. De maneira geral, isso faz pouco sentido - você não deve verificar as bibliotecas padrão - mas também esse código leva a outro problema - para aumentar o tempo de execução dos testes por um segundo. Parece que isso não é muito ... Mas multiplique esse segundo pelo número de testes semelhantes, pelo número de desenvolvedores, pelo número de lançamentos por dia ... E você entenderá que, devido a esses tempos limites, pode perder muitas horas de trabalho semanalmente, se não diariamente.
4)
const fs = require('fs'); const testData = JSON.parse(fs.readFileSync('./testData.json', 'utf8')); describe('some block', ()=>{ it('should do something', ()=>{ someTest(testData); }) })
O que há de errado aquiCarregando dados de teste fora dos blocos de teste.
À primeira vista, parece que não importa onde ler os dados de teste - na descrição, no bloco ou no próprio módulo. No segundo também. Mas imagine que você tenha centenas de testes e muitos deles usam dados pesados. Se você carregá-los fora do teste, isso levará ao fato de que todos os dados de teste permanecerão na memória até o final da execução do teste e, com o tempo, o lançamento consumirá mais e mais RAM - até que os testes não sejam mais executados. máquinas de trabalho padrão.
5)
const {assert} = require('chai'); const sinon = require('sinon'); class Dog {
O que há de errado aquiO código é realmente substituído por stubs.
Certamente você viu imediatamente esse erro ridículo. No código real, é claro que isso não é tão óbvio - mas vi um código tão cheio de stubs que não testei nada.
6
const sinon = require('sinon'); const {assert} = require('chai'); class Widget { fetch() {} loadData() { this.fetch(); } } if (!sinon.sandbox || !sinon.sandbox.stub) { sinon.sandbox = sinon.createSandbox(); } describe('My widget', () => { it('is awesome', () => { const widget = new Widget(); widget.fetch = sinon.sandbox.stub().returns({ one: 1, two: 2 }); widget.loadData(); assert.isTrue(widget.fetch.called); }); });
O que há de errado aquiDependência entre testes.
À primeira vista, é claro que eles se esqueceram de escrever aqui
afterEach(() => { sinon.sandbox.restore(); });
Mas o problema não é apenas isso, mas que a mesma caixa de proteção é usada para todos os testes. E é muito fácil confundir o ambiente de execução de teste de forma que eles comecem a depender um do outro. Depois disso, os testes começarão a ser realizados apenas em uma determinada ordem e, em geral, não está claro o que testar.
Felizmente, em algum momento, o sinon.sandbox foi declarado obsoleto e cortado, portanto, você só pode encontrar esse problema em um projeto legado - mas existem muitas outras maneiras de confundir o ambiente de execução de teste de forma que será doloroso investigar mais tarde. qual código é culpado de comportamento incorreto. A propósito, recentemente houve um post em um hub sobre algum tipo de modelo como “Fábrica de Gelo” - isso não é uma panacéia, mas às vezes ajuda nesses casos.
7. Enormes dados de teste no arquivo de teste
Muitas vezes eu vi como enormes arquivos JSON, e até XML, estavam diretamente no teste. Acho óbvio por que não vale a pena fazer isso - torna-se doloroso assistir, editar e qualquer IDE não agradece por isso. Se você tiver dados de teste grandes, retire-os do arquivo de teste.
8)
const {assert} = require('chai'); const crypto = require('crypto'); describe('extraTests', ()=>{ it('should generate unique bytes', ()=>{ const arr = []; for (let i = 0; i < 1000; i++) { const value = crypto.randomBytes(256); arr.push(value); } const unique = arr.filter((el, index)=>arr.indexOf(el) === index); assert.equal(arr.length, unique.length, 'Data is not random enough!'); }); });
O que há de errado aquiTestes extras.
Nesse caso, o desenvolvedor estava muito preocupado com o fato de seus identificadores únicos serem únicos, então ele fez uma verificação para isso. Em geral, um desejo compreensível - mas é melhor ler a documentação ou executar esse teste várias vezes sem adicioná-lo ao projeto. Executá-lo em todas as versões não faz sentido.
Bem, o empate por valores aleatórios no teste é, por si só, uma ótima maneira de dar um tiro no próprio pé, fazendo um teste instável do zero.
9. Falta de mok
É muito mais fácil executar testes com um banco de dados ativo e 100% de serviços e executar testes neles.
Porém, mais cedo ou mais tarde ele voltará a funcionar - os testes de remoção de dados serão executados na base de produtos, começarão a cair devido a um serviço de parceiro com defeito ou o seu IC simplesmente não terá uma base para executá-los. Em geral, o item é bastante holístico, mas como regra - se você pode emular serviços externos, é melhor fazê-lo.
11)
const {assert} = require('chai'); class CustomError extends Error { } function mytestFunction() { throw new CustomError('important message'); } describe('badCompare', ()=>{ it('should throw only my custom errors', ()=>{ let errorHappened = false; try { mytestFunction(); } catch (err) { errorHappened = true; assert.isTrue(err instanceof CustomError); } assert.isTrue(errorHappened); }); });
O que há de errado aquiDepuração de erro complicada.
Tudo não está ruim, mas há um problema - se o teste falhar repentinamente, você verá um erro no formulário
1) badCompare
should throw only my custom errors:
AssertionError: expected false to be true
+ expected - actual
-false
+true
at Context.it (test/011_badCompare/test.js:23:14)
Além disso, para entender que tipo de erro realmente aconteceu - é necessário reescrever o teste. Portanto, no caso de um erro inesperado - tente fazer o teste falar sobre isso, e não apenas o fato de que aconteceu.
12)
const {assert} = require('chai'); function someVeryBigFunc1() { return 1;
O que há de errado aquiTudo no bloco anterior.
Parece que uma abordagem legal é fazer todas as operações no bloco `before` e, assim, deixar apenas verificações dentro do` it`.
Na verdade não.
Porque, neste caso, há uma confusão na qual você não pode entender o tempo da execução real dos testes, nem o motivo da queda, nem o que se relaciona a um teste e o que a outro.
Portanto, todo o trabalho do teste (exceto as inicializações padrão) deve ser realizado dentro do próprio teste.
13)
const {assert} = require('chai'); const moment = require('moment'); function someDateBasedFunction(date) { if (moment().isAfter(date)) { return 0; } return 1; } describe('useFutureDate', ()=>{ it('should return 0 for passed date', ()=>{ const pastDate = moment('2010-01-01'); assert.equal(someDateBasedFunction(pastDate), 0); }); it('should return 1 for future date', ()=>{ const itWillAlwaysBeInFuture = moment('2030-01-01'); assert.equal(someDateBasedFunction(itWillAlwaysBeInFuture), 1); }); });
O que há de errado aquiGravata em datas.
Também parece um erro óbvio - mas também surge periodicamente entre desenvolvedores cansados que já acreditam que o amanhã nunca chegará. E a construção que estava indo bem ontem, de repente cai hoje.
Lembre-se de que qualquer data chegará mais cedo ou mais tarde - então use a emulação de tempo com coisas como `sinon.fakeTimers` ou pelo menos defina datas remotas como 2050 - deixe seus descendentes magoados ...
14)
describe('dynamicRequires', ()=>{ it('should return english locale', ()=>{
O que há de errado aquiCarregamento dinâmico de módulos.
Se você possui o Eslint, provavelmente já baniu dependências dinâmicas. Ou não
Frequentemente, vejo que os desenvolvedores estão tentando carregar bibliotecas ou vários módulos diretamente dentro dos testes. Ao mesmo tempo, eles geralmente sabem como o `exigem` funciona - mas preferem a ilusão de que devem receber um módulo limpo que ninguém confundiu até agora.
Essa suposição é perigosa, pois o carregamento de módulos adicionais durante os testes é mais lento e, mais uma vez, leva a um comportamento mais indefinido.
15
function someComplexFunc() {
O que há de errado aquiNomes de teste incompreensíveis.
Você deve estar cansado de coisas óbvias, certo? Mas você ainda precisa dizer isso, porque muitos não se preocupam em escrever nomes compreensíveis para os testes - e, como resultado, é possível entender o que um teste em particular faz apenas após muita pesquisa.
16
const {assert} = require('chai'); const Promise = require('bluebird'); function someTomeoutingFunction() { throw new Promise.TimeoutError(); } describe('no Error check', ()=>{ it('should throw error', async ()=>{ let timedOut = false; try { await someTomeoutingFunction(); } catch (err) { timedOut = true; } assert.equal(timedOut, true); }); });
O que há de errado aquiFalta de verificação do erro lançado.
Geralmente, você precisa verificar se, em alguns casos, a função gera um erro. Mas você sempre precisa verificar se estes são os dróides que procuramos - pois, de repente, pode acontecer que outro erro foi lançado, em outro lugar e por outros motivos ...
17
function someBadFunc() { throw new Error('I am just wrong!'); } describe.skip('skipped test', ()=>{ it('should be fine', ()=>{ someBadFunc(); }); });
O que há de errado aquiTestes desativados.
Obviamente, uma situação sempre pode surgir quando o código já foi testado várias vezes com as mãos, você precisa revertê-lo com urgência e, por algum motivo, o teste não funciona. Por exemplo, devido à complicação não óbvia de outro teste, sobre o qual escrevi anteriormente. E o teste está desligado. E isso é normal. Não é normal - não defina imediatamente a tarefa de reativar o teste. Se isso não for feito, o número de testes desabilitados se multiplicará e o código ficará obsoleto constantemente. Até que a única opção permaneça - mostre misericórdia e faça todos esses testes nafig, porque é mais rápido escrevê-los novamente do que entender os erros.
Aqui está essa seleção saiu. Todos esses testes passam bem nos testes, mas são quebrados por design. Adicione suas opções nos comentários ou no
repositório que fiz para coletar esses erros.