Neulich werde ich einen internen Bericht erstellen, in dem ich unseren Entwicklern die unangenehmen Fehler erläutere, die beim Schreiben von Komponententests auftreten können. Die aus meiner Sicht unangenehmsten Fehler sind, wenn die Tests bestanden werden, aber gleichzeitig machen sie es so falsch, dass es besser wäre, nicht zu bestehen. Und ich habe beschlossen, Beispiele für solche Fehler mit allen zu teilen. Sicherlich noch etwas aus dieser Gegend zu erzählen. Beispiele sind für Node.JS und Mocha geschrieben, aber im Allgemeinen gelten diese Fehler für jedes andere Ökosystem.
Um es interessanter zu machen, sind einige von ihnen in Form eines Problemcodes und eines Spoilers eingerahmt, der öffnet, und Sie werden sehen, was das Problem war. Ich empfehle daher, dass Sie sich zuerst den Code ansehen, einen Fehler darin finden und dann den Spoiler öffnen. Es wird keine Lösung für die Probleme angegeben - ich schlage vor, selbst darüber nachzudenken. Nur weil ich faul bin. Die Reihenfolge der Liste macht keinen Sinn - es ist nur eine Sequenz, in der ich mich an alle möglichen wirklichen Probleme erinnerte, die uns zu blutigen Tränen brachten. Sicherlich werden Ihnen viele Dinge offensichtlich erscheinen - aber selbst erfahrene Entwickler können versehentlich solchen Code schreiben.
Also lass uns gehen.
0. Fehlende Tests
Seltsamerweise glauben viele immer noch, dass das Schreiben von Tests die Entwicklungsgeschwindigkeit verlangsamt. Natürlich ist es offensichtlich, dass mehr Zeit für das Schreiben von Tests und das Schreiben von Code aufgewendet werden muss, der getestet werden kann. Aber nach dem Debuggen und Regressionen müssen Sie viel mehr Zeit verbringen ...
1. Das Fehlen laufender Tests
Wenn Sie Tests haben, die Sie nicht oder von Zeit zu Zeit ausführen, ist dies wie das Fehlen von Tests. Und es ist noch schlimmer - Sie haben einen veralteten Testcode und ein falsches Sicherheitsgefühl. Tests sollten mindestens in CI-Prozessen ausgeführt werden, wenn Code in einen Zweig verschoben wird. Und besser - lokal vor dem Push. Dann muss der Entwickler in wenigen Tagen nicht mehr zum Build zurückkehren, was, wie sich herausstellte, nicht bestanden hat.
2. Mangelnde Deckung
Wenn Sie immer noch nicht wissen, welche Abdeckung in Tests enthalten ist, ist es jetzt an der Zeit, zu lesen. Zumindest
Wikipedia . Andernfalls besteht eine gute Chance, dass Ihr Test 10% des Codes überprüft, von dem Sie glauben, dass er überprüft wird. Früher oder später werden Sie definitiv darauf treten. Selbst eine 100% ige Abdeckung des Codes garantiert natürlich in keiner Weise seine vollständige Richtigkeit - aber dies ist viel besser als die mangelnde Abdeckung, da es Ihnen viel mehr potenzielle Fehler zeigt. Kein Wunder, dass die neuesten Versionen von Node.JS sogar integrierte Tools zum Lesen haben. Im Allgemeinen ist das Thema Berichterstattung tiefgreifend und äußerst ganzheitlich, aber ich werde nicht zu tief darauf eingehen - ich möchte ein wenig über viel sagen.
3.
const {assert} = require('chai'); const Promise = require('bluebird'); const sinon = require('sinon'); class MightyLibrary { static someLongFunction() { return Promise.resolve(1);
Was ist hier los?Zeitüberschreitungen bei Unit-Tests.
Hier wollten sie überprüfen, ob das Einstellen von Zeitüberschreitungen für einen langen Vorgang wirklich funktioniert. Im Allgemeinen macht dies ohnehin wenig Sinn - Sie sollten die Standardbibliotheken nicht überprüfen - aber auch dieser Code führt zu einem anderen Problem -, um die Ausführungszeit von Tests für eine Sekunde zu verlängern. Es scheint, dass dies nicht so viel ist ... Aber multiplizieren Sie diese Sekunde mit der Anzahl ähnlicher Tests, mit der Anzahl der Entwickler, mit der Anzahl der Starts pro Tag ... Und Sie werden verstehen, dass Sie aufgrund solcher Zeitüberschreitungen wöchentlich viele Stunden Arbeit verlieren können, wenn nicht sogar täglich.
4.
const fs = require('fs'); const testData = JSON.parse(fs.readFileSync('./testData.json', 'utf8')); describe('some block', ()=>{ it('should do something', ()=>{ someTest(testData); }) })
Was ist hier los?Laden von Testdaten außerhalb von Testblöcken.
Auf den ersten Blick scheint es egal zu sein, wo die Testdaten gelesen werden sollen - in der Beschreibung, im Block oder im Modul selbst. Auch beim zweiten. Stellen Sie sich jedoch vor, Sie haben Hunderte von Tests, von denen viele umfangreiche Daten verwenden. Wenn Sie sie außerhalb des Tests laden, führt dies dazu, dass alle Testdaten bis zum Ende der Testausführung im Speicher verbleiben und der Start im Laufe der Zeit immer mehr RAM verbraucht - bis sich herausstellt, dass die Tests überhaupt nicht mehr ausgeführt werden Standardarbeitsmaschinen.
5.
const {assert} = require('chai'); const sinon = require('sinon'); class Dog {
Was ist hier los?Der Code wird tatsächlich durch Stubs ersetzt.
Sicher haben Sie diesen lächerlichen Fehler sofort gesehen. In echtem Code ist dies natürlich nicht so offensichtlich - aber ich habe Code gesehen, der so mit Stubs aufgehängt war, dass ich überhaupt nichts getestet habe.
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); }); });
Was ist hier los?Abhängigkeit zwischen Tests.
Auf den ersten Blick ist klar, dass sie vergessen haben, hier zu schreiben
afterEach(() => { sinon.sandbox.restore(); });
Das Problem ist aber nicht nur das, sondern dass für alle Tests dieselbe Sandbox verwendet wird. Und es ist sehr leicht, die Testausführungsumgebung so zu verwechseln, dass sie voneinander abhängig werden. Danach werden die Tests nur in einer bestimmten Reihenfolge durchgeführt, und im Allgemeinen ist nicht klar, was getestet werden soll.
Glücklicherweise wurde sinon.sandbox irgendwann für veraltet und ausgeschnitten erklärt, sodass Sie nur bei einem Legacy-Projekt auf ein solches Problem stoßen können. Es gibt jedoch so viele andere Möglichkeiten, die Testausführungsumgebung so zu verwirren, dass eine spätere Untersuchung schmerzhaft schmerzhaft ist. Welcher Code ist an falschem Verhalten schuld? Übrigens gab es kürzlich einen Beitrag in einem Hub über eine Art Vorlage wie „Ice Factory“ - dies ist kein Allheilmittel, aber manchmal hilft es in solchen Fällen.
7. Riesige Testdaten in der Testdatei
Sehr oft habe ich gesehen, wie große JSON-Dateien und sogar XML direkt im Test lagen. Ich denke, es ist offensichtlich, warum sich dies nicht lohnt - es wird schmerzhaft, es anzusehen, zu bearbeiten, und jede IDE wird es Ihnen nicht danken. Wenn Sie große Testdaten haben, nehmen Sie diese aus der Testdatei.
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!'); }); });
Was ist hier los?Zusätzliche Tests.
In diesem Fall war der Entwickler sehr besorgt, dass seine eindeutigen Kennungen eindeutig sein würden, und schrieb daher einen Scheck dafür. Im Allgemeinen ein verständlicher Wunsch - aber es ist besser, die Dokumentation zu lesen oder einen solchen Test mehrmals durchzuführen, ohne ihn dem Projekt hinzuzufügen. Es macht keinen Sinn, es in jedem Build auszuführen.
Nun, das Unentschieden für zufällige Werte im Test ist an sich schon eine großartige Möglichkeit, sich in den Fuß zu schießen, indem Sie einen instabilen Test von Grund auf neu durchführen.
9. Mangel an Mok
Es ist viel einfacher, Tests mit einer Live-Datenbank und 100-prozentigen Diensten auszuführen und Tests mit ihnen durchzuführen.
Aber früher oder später wird es wieder Früchte tragen - Datenentfernungstests werden auf der Produktbasis durchgeführt, fallen aufgrund eines defekten Partnerservices ab oder Ihr CI verfügt einfach nicht über eine Basis, auf der sie ausgeführt werden können. Im Allgemeinen ist das Element ziemlich ganzheitlich, aber in der Regel - wenn Sie externe Dienste emulieren können, ist es besser, dies zu tun.
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); }); });
Was ist hier los?Kompliziertes Fehler-Debugging.
Alles ist nicht schlecht, aber es gibt ein Problem: Wenn der Test plötzlich abstürzt, wird ein Fehler im Formular angezeigt
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)
Um zu verstehen, welche Art von Fehler tatsächlich aufgetreten ist, müssen Sie den Test neu schreiben. Versuchen Sie also im Falle eines unerwarteten Fehlers, den Test darüber sprechen zu lassen, und nicht nur die Tatsache, dass es passiert ist.
12.
const {assert} = require('chai'); function someVeryBigFunc1() { return 1;
Was ist hier los?Alles im Vorblock.
Es scheint, dass ein cooler Ansatz darin besteht, alle Operationen im "Vorher" -Block auszuführen und daher nur Überprüfungen im "Es" zu belassen.
Nicht wirklich.
Denn in diesem Fall gibt es ein Durcheinander, in dem Sie weder den Zeitpunkt der tatsächlichen Ausführung der Tests noch den Grund für den Sturz verstehen können, noch was sich auf einen Test bezieht und was auf einen anderen.
Daher sollte die gesamte Arbeit des Tests (mit Ausnahme von Standardinitialisierungen) innerhalb des Tests selbst ausgeführt werden.
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); }); });
Was ist hier los?Termine binden.
Es scheint auch ein offensichtlicher Fehler zu sein - aber es tritt auch regelmäßig bei müden Entwicklern auf, die bereits glauben, dass morgen niemals kommen wird. Und der Build, der gestern gut lief, fällt heute plötzlich ab.
Denken Sie daran, dass jedes Datum früher oder später kommen wird - verwenden Sie also entweder die Zeitemulation mit Dingen wie "sinon.fakeTimers" oder setzen Sie zumindest entfernte Daten wie 2050 - lassen Sie Ihre Nachkommen verletzen ...
14.
describe('dynamicRequires', ()=>{ it('should return english locale', ()=>{
Was ist hier los?Dynamisches Laden von Modulen.
Wenn Sie Eslint haben, haben Sie wahrscheinlich bereits dynamische Abhängigkeiten gesperrt. Oder nicht.
Oft sehe ich, dass Entwickler versuchen, Bibliotheken oder verschiedene Module direkt in die Tests zu laden. Sie wissen jedoch im Allgemeinen, wie "erfordern" funktioniert - aber sie bevorzugen die Illusion, dass sie ein sauberes Modul erhalten sollen, das bisher niemand verwirrt hat.
Diese Annahme ist insofern gefährlich, als das Laden zusätzlicher Module während der Tests langsamer ist und wiederum zu einem undefinierteren Verhalten führt.
15.
function someComplexFunc() {
Was ist hier los?Unverständliche Testnamen.
Sie müssen müde von offensichtlichen Dingen sein, oder? Aber Sie müssen immer noch dazu sagen, weil sich viele nicht die Mühe machen, verständliche Namen für die Tests zu schreiben - und als Ergebnis ist es möglich zu verstehen, was ein bestimmter Test nur nach viel Forschung tut.
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); }); });
Was ist hier los?Fehlende Überprüfung des ausgelösten Fehlers.
Oft müssen Sie überprüfen, ob die Funktion in einigen Fällen einen Fehler auslöst. Aber Sie müssen immer überprüfen, ob dies die Droiden sind, nach denen wir suchen - da sich plötzlich herausstellen kann, dass ein anderer Fehler an einem anderen Ort und aus anderen Gründen aufgetreten ist ...
17.
function someBadFunc() { throw new Error('I am just wrong!'); } describe.skip('skipped test', ()=>{ it('should be fine', ()=>{ someBadFunc(); }); });
Was ist hier los?Deaktivierte Tests.
Natürlich kann es immer dann vorkommen, dass der Code bereits viele Male mit Ihren Händen getestet wurde, Sie ihn dringend rollen müssen und der Test aus irgendeinem Grund nicht funktioniert. Zum Beispiel aufgrund der nicht offensichtlichen Komplikation eines anderen Tests, über den ich zuvor geschrieben habe. Und der Test ist ausgeschaltet. Und das ist normal. Nicht normal - Stellen Sie den Test nicht sofort wieder ein. Wenn dies nicht getan wird, vervielfacht sich die Anzahl der deaktivierten Tests und ihr Code wird ständig veraltet. Bis die einzige Möglichkeit bleibt - zeigen Sie Gnade und werfen Sie all diese Tests nafig, denn es ist schneller, sie erneut zu schreiben, als die Fehler zu verstehen.
Hier ist eine solche Auswahl herausgekommen. Alle diese Tests bestehen die Tests gut, sind jedoch vom Design her fehlerhaft. Fügen Sie Ihre Optionen in den Kommentaren oder im
Repository hinzu, das ich erstellt habe, um solche Fehler zu sammeln.