Errores desagradables al escribir pruebas unitarias

El otro día, haré un informe interno en el que les contaré a nuestros desarrolladores sobre los errores desagradables que pueden ocurrir al escribir pruebas unitarias. Los errores más desagradables desde mi punto de vista son cuando pasan las pruebas, pero al mismo tiempo lo hacen tan incorrectamente que sería mejor no pasar. Y decidí compartir ejemplos de tales errores con todos. Seguramente algo más que contar de esta área. Se escriben ejemplos para Node.JS y Mocha, pero en general estos errores son ciertos para cualquier otro ecosistema.

Para hacerlo más interesante, algunos de ellos están enmarcados en forma de un código de problema y un spoiler, abriendo el cual, verá cuál fue el problema. Por lo tanto, le recomiendo que primero mire el código, encuentre un error en él y luego abra el spoiler. No se indicará ninguna solución a los problemas. Propongo pensarlo nosotros mismos. Solo porque soy vago. El orden de la lista no tiene mucho sentido: es simplemente una secuencia en la que recordé todo tipo de problemas reales que nos llevaron a lágrimas de sangre. Seguramente muchas cosas te parecerán obvias, pero incluso los desarrolladores experimentados pueden escribir accidentalmente dicho código.


Entonces vamos.

0. Falta de pruebas


Por extraño que parezca, muchos todavía creen que escribir pruebas ralentiza la velocidad de desarrollo. Por supuesto, es obvio que se debe dedicar más tiempo a escribir pruebas y escribir código que pueda probarse. Pero después de la depuración y la regresión, debes pasar muchas veces más tiempo ...

1. La falta de pruebas en ejecución


Si tiene pruebas que no ejecuta, o ejecuta de vez en cuando, entonces esto es como la ausencia de pruebas. Y es aún peor: tiene un código de prueba desactualizado y una falsa sensación de seguridad. Las pruebas al menos deberían ejecutarse en los procesos de CI al insertar código en una rama. Y mejor, localmente antes del empuje. Luego, el desarrollador no tendrá que volver a la compilación en unos días, lo que resultó que no pasó.

2. Falta de cobertura


Si aún no sabe qué cobertura hay en las pruebas, es hora de ir a leer ahora mismo. Al menos Wikipedia . De lo contrario, existe una buena posibilidad de que su prueba verifique el 10% del código que usted cree que verifica. Tarde o temprano definitivamente lo pisarás. Por supuesto, incluso el 100% de cobertura del código no garantiza su corrección completa de ninguna manera, pero esto es mucho mejor que la falta de cobertura, ya que le mostrará muchos más posibles errores. No es de extrañar que las últimas versiones de Node.JS incluso tengan herramientas integradas para leerlo. En general, el tema de la cobertura es profundo y extremadamente holístico, pero no voy a profundizar demasiado en ello: quiero decir un poco sobre mucho.

3)



const {assert} = require('chai'); const Promise = require('bluebird'); const sinon = require('sinon'); class MightyLibrary { static someLongFunction() { return Promise.resolve(1); // just imagine a really complex and long function here } } async function doItQuickOrFail() { let res; try { res = await MightyLibrary.someLongFunction().timeout(1000); } catch (err) { if (err instanceof Promise.TimeoutError) { return false; } throw err; } return res; } describe('using Timeouts', ()=>{ it('should return false if waited too much', async ()=>{ // stub function to emulate looong work sinon.stub(MightyLibrary, 'someLongFunction').callsFake(()=>Promise.delay(10000).then(()=>true)); const res = await doItQuickOrFail(); assert.equal(res, false); }); }); 


Que pasa aqui
Tiempos de espera en pruebas unitarias.

Aquí querían comprobar que la configuración de tiempos de espera para una operación larga realmente funciona. En general, esto tiene poco sentido de todos modos, no debe verificar las bibliotecas estándar, pero también este código conduce a otro problema, para aumentar el tiempo de ejecución de las pruebas por un segundo. Parecería que esto no es tanto ... Pero multiplique este segundo por la cantidad de pruebas similares, por la cantidad de desarrolladores, por la cantidad de lanzamientos por día ... Y comprenderá que debido a estos tiempos de espera puede perder muchas horas de trabajo semanalmente, si no diariamente.



4)



 const fs = require('fs'); const testData = JSON.parse(fs.readFileSync('./testData.json', 'utf8')); describe('some block', ()=>{ it('should do something', ()=>{ someTest(testData); }) }) 


Que pasa aqui
Carga de datos de prueba fuera de los bloques de prueba.

A primera vista, parece que no importa dónde leer los datos de prueba: en la descripción, el bloque o en el módulo en sí. En el segundo también. Pero imagine que tiene cientos de pruebas, y muchas de ellas usan datos pesados. Si los carga fuera de la prueba, esto conducirá al hecho de que todos los datos de la prueba permanecerán en la memoria hasta el final de la ejecución de la prueba, y el lanzamiento con el tiempo consumirá más y más RAM, hasta que las pruebas ya no se ejecuten en absoluto. Máquinas de trabajo estándar.



5)



 const {assert} = require('chai'); const sinon = require('sinon'); class Dog { // eslint-disable-next-line class-methods-use-this say() { return 'Wow'; } } describe('stubsEverywhere', ()=>{ before(()=>{ sinon.stub(Dog.prototype, 'say').callsFake(()=>{ return 'meow'; }); }); it('should say meow', ()=>{ const dog = new Dog(); assert.equal(dog.say(), 'meow', 'dog should say "meow!"'); }); }); 


Que pasa aqui
El código se reemplaza realmente por apéndices.

Seguramente de inmediato viste este ridículo error. En el código real, esto, por supuesto, no es tan obvio, pero vi un código que estaba tan colgado de trozos que no probé nada en absoluto.



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); }); }); 


Que pasa aqui
Dependencia entre pruebas.

A primera vista, está claro que se olvidaron de escribir aquí.

  afterEach(() => { sinon.sandbox.restore(); }); 


Pero el problema no es solo esto, sino que se usa el mismo sandbox para todas las pruebas. Y es muy fácil confundir el entorno de ejecución de la prueba de tal manera que comienzan a depender el uno del otro. Después de eso, las pruebas comenzarán a realizarse solo en un cierto orden, y en general no está claro qué probar.

Afortunadamente, en algún momento, sinon.sandbox fue declarado obsoleto y cortado, por lo que solo puede encontrar un problema de este tipo en un proyecto heredado, pero hay muchas otras formas de confundir el entorno de ejecución de la prueba de tal manera que será dolorosamente doloroso investigar más tarde. qué código es culpable de comportamiento incorrecto. Por cierto, recientemente hubo una publicación en un centro sobre algún tipo de plantilla como "Ice Factory"; esto no es una panacea, pero a veces ayuda en tales casos.




7. Datos de prueba enormes en el archivo de prueba



Muy a menudo vi cómo enormes archivos JSON, e incluso XML, yacían directamente en la prueba. Creo que es obvio por qué no vale la pena hacerlo: resulta doloroso verlo, editarlo y cualquier IDE no lo agradecerá. Si tiene datos de prueba grandes, retírelos del archivo de prueba.

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!'); }); }); 


Que pasa aqui
Pruebas extra

En este caso, el desarrollador estaba muy preocupado de que sus identificadores únicos fueran únicos, por lo que escribió un cheque para esto. En general, un deseo comprensible, pero es mejor leer la documentación o ejecutar dicha prueba varias veces sin agregarla al proyecto. Ejecutarlo en cada compilación no tiene sentido.

Bueno, el empate de valores aleatorios en la prueba es en sí mismo una excelente manera de dispararse en el pie al hacer una prueba inestable desde cero.



9. Falta de mok


Es mucho más fácil ejecutar pruebas con una base de datos en vivo y servicios 100 por ciento, y ejecutar pruebas en ellas.
Pero tarde o temprano volverá a concretarse: las pruebas de eliminación de datos se ejecutarán en la base del producto, comenzarán a caer debido a un servicio de socio roto, o su CI simplemente no tendrá una base sobre la cual ejecutarlas. En general, el elemento es bastante holístico, pero como regla general, si puede emular servicios externos, entonces es mejor hacerlo.

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); }); }); 


Que pasa aqui
Error de depuración complicado.

No todo está mal, pero hay un problema: si la prueba falla repentinamente, verá un error en el formulario

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)


Además, para comprender qué tipo de error ocurrió realmente, debe volver a escribir la prueba. Entonces, en caso de un error inesperado, intente que la prueba hable sobre ello, y no solo sobre el hecho de que sucedió.



12)


 const {assert} = require('chai'); function someVeryBigFunc1() { return 1; // imagine a tonn of code here } function someVeryBigFunc2() { return 2; // imagine a tonn of code here } describe('all Before Tests', ()=>{ let res1; let res2; before(async ()=>{ res1 = await someVeryBigFunc1(); res2 = await someVeryBigFunc2(); }); it('should return 1', ()=>{ assert.equal(res1, 1); }); it('should return 2', ()=>{ assert.equal(res2, 2); }); }); 


Que pasa aqui
Todo en el bloque anterior.

Parecería que un enfoque genial es hacer todas las operaciones en el bloque `before` y, por lo tanto, dejar solo verificaciones dentro del` it`.
En realidad no
Porque en este caso hay un desorden en el que no puedes entender el momento de la ejecución real de las pruebas, ni la razón de la caída, ni lo que se relaciona con una prueba, y qué con otra.
Por lo tanto, todo el trabajo de la prueba (excepto las inicializaciones estándar) debe realizarse dentro de la prueba misma.



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); }); }); 


Que pasa aqui
Empate en las fechas.

También parecería un error obvio, pero también surge periódicamente entre los desarrolladores cansados ​​que ya creen que el mañana nunca llegará. Y la construcción que iba bien ayer de repente cae hoy.

Recuerde que cualquier fecha llegará tarde o temprano, así que use la emulación de tiempo con cosas como `sinon.fakeTimers`, o al menos establezca fechas remotas como 2050, deje que sus descendientes sufran ...



14)



 describe('dynamicRequires', ()=>{ it('should return english locale', ()=>{ // HACK : // Some people mutate locale in tests to chinese so I will require moment here // eslint-disable-next-line global-require const moment = require('moment'); const someDate = moment('2010-01-01').format('MMMM'); assert.equal(someDate, 'January'); }); }); 


Que pasa aqui
Carga dinámica de módulos.

Si tiene Eslint, entonces probablemente ya haya prohibido las dependencias dinámicas. O no
A menudo veo que los desarrolladores están intentando cargar bibliotecas o varios módulos directamente dentro de las pruebas. Sin embargo, generalmente saben cómo funciona `require`, pero prefieren la ilusión de que se supone que se les debe dar un módulo limpio que nadie ha confundido hasta ahora.
Esta suposición es peligrosa porque la carga de módulos adicionales durante las pruebas es más lenta y nuevamente conduce a un comportamiento más indefinido.



15)


 function someComplexFunc() { // Imagine a piece of really strange code here return 1; } describe('cryptic', ()=>{ it('success', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); it('should not fail', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); it('is right', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); it('makes no difference for solar system', ()=>{ const result = someComplexFunc(); assert.equal(result, 1); }); }); 


Que pasa aqui
Nombres de prueba incomprensibles.

Debes estar cansado de cosas obvias, ¿verdad? Pero todavía tiene que decirlo porque muchos no se molestan en escribir nombres comprensibles para las pruebas y, como resultado, es posible entender lo que hace una prueba en particular solo después de mucha investigación.



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); }); }); 


Que pasa aqui
Falta de verificación del error arrojado.

A menudo, debe verificar que en algún caso la función arroje un error. Pero siempre debe verificar si estos son los droides que estamos buscando, ya que de repente puede resultar que se arrojó un error diferente, en otro lugar y por otras razones ...



17)



 function someBadFunc() { throw new Error('I am just wrong!'); } describe.skip('skipped test', ()=>{ it('should be fine', ()=>{ someBadFunc(); }); }); 


Que pasa aqui
Pruebas deshabilitadas.

Por supuesto, una situación siempre puede surgir cuando el código ya ha sido probado muchas veces con sus manos, necesita rodarlo con urgencia y, por alguna razón, la prueba no funciona. Por ejemplo, debido a la complicación no obvia de otra prueba, sobre la que escribí anteriormente. Y la prueba está apagada. Y esto es normal. No es normal: no configure inmediatamente la tarea de volver a encender la prueba. Si esto no se hace, la cantidad de pruebas deshabilitadas se multiplicará y su código se volverá obsoleto constantemente. Hasta que la única opción permanezca: muestre misericordia y arroje todas estas pruebas a la ligera, porque es más rápido volver a escribirlas que comprender los errores.



Aquí está tal selección salió. Todas estas pruebas pasan bien las pruebas, pero están rotas por diseño. Agregue sus opciones en los comentarios o en el repositorio que hice para recopilar dichos errores.

Source: https://habr.com/ru/post/es432092/


All Articles