Unangenehme Fehler beim Schreiben von Komponententests

    Neulich werde ich einen internen Bericht erstellen, in dem ich unsere Entwickler über die unangenehmen Fehler unterrichte, die beim Schreiben von Komponententests auftreten können. Die unangenehmsten Fehler sind aus meiner Sicht, wenn Tests bestanden werden, gleichzeitig tun sie es jedoch so falsch, dass es besser wäre, nicht zu bestehen. Und ich habe beschlossen, Beispiele für solche Fehler mit allen zu teilen. Sicherlich sagt mir etwas anderes aus dieser Gegend. Beispiele werden für Node.JS und Mocha geschrieben, aber im Allgemeinen gelten diese Fehler für jedes andere Ökosystem.

    Um es noch interessanter zu machen, sind einige von ihnen in Form eines Problemcodes und eines Spoilers umrahmt, die Sie öffnen können, um zu sehen, was das Problem war. Ich empfehle Ihnen, zuerst den Code zu betrachten, einen Fehler darin zu finden und dann den Spoiler zu öffnen. Lösungen für die Probleme werden nicht angezeigt - ich schlage vor, Sie denken selbst darüber nach. Nur weil ich faul bin. Die Reihenfolge der Liste hat keine tiefe Bedeutung - es ist nur eine Reihenfolge, in der ich mich an alle möglichen Probleme erinnerte, die uns zu blutigen Tränen gebracht haben. Sicherlich werden Ihnen viele Dinge offensichtlich sein - 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 Sie mehr Zeit damit verbringen müssen, Tests zu schreiben und Code zu schreiben, der getestet werden kann. Nach dem Debuggen und Regressionen muss dann jedoch viel mehr Zeit aufgewendet werden ...

    1. Kein Testlauf


    Wenn Sie über Tests verfügen, die Sie nicht ausführen, oder von Zeit zu Zeit ausgeführt werden, ist dies wie das Fehlen von Tests. Und es ist noch schlimmer - Sie haben einen alternden Testcode und ein falsches Sicherheitsgefühl. Tests sollten zumindest in CI-Prozessen ausgeführt werden, wenn Code in eine Verzweigung übertragen wird. Und es ist besser - lokal, bevor wir drängen. Dann muss der Entwickler nicht innerhalb weniger Tage zum Build zurückkehren, was jedoch nicht funktioniert hat.

    2. Fehlende Abdeckung


    Wenn Sie immer noch nicht wissen, was die Beschichtung in den Tests ist, ist es jetzt an der Zeit, zu lesen. Zumindest Wikipedia . Ansonsten besteht eine große Chance, dass Ihr Test die Stärke von 10% des Codes überprüft, den Sie Ihrer Meinung nach prüfen. Früher oder später wirst du definitiv darauf treten. Natürlich garantiert auch eine 100% ige Abdeckung des Codes in keiner Weise seine vollständige Korrektheit - dies ist jedoch viel besser als die fehlende Abdeckung, da hier viel mehr potenzielle Fehler angezeigt werden. Kein Wunder, dass in den neuesten Versionen von Node.JS sogar integrierte Tools zum Zählen vorhanden waren. Im Allgemeinen ist das Thema der Berichterstattung tiefgreifend und extrem holivar, aber ich werde nicht zu viel darauf eingehen - ich möchte einiges zu viel sagen.

    3



    const {assert} = require('chai');
    constPromise = require('bluebird');
    const sinon = require('sinon');
    classMightyLibrary{
      static someLongFunction() {
        returnPromise.resolve(1); // just imagine a really complex and long function here
      }
    }
    asyncfunctiondoItQuickOrFail()
    {
      let res;
      try {
        res = await MightyLibrary.someLongFunction().timeout(1000);
      }
      catch (err)
      {
        if (err instanceofPromise.TimeoutError)
        {
          returnfalse;
        }
        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);
      });
    });


    Was ist hier los?
    Таймауты в юнит тестах.

    Здесь хотели проверить, что установка таймаутов на долгую операцию действительно работает. В целом это и так имеет мало смысла — не стоит проверять стандартные библиотеки — но так же такой код приводит к другой проблеме — увеличению выполнения времени прохождения тестов на секунду. Казалось бы, это не так много… Но помножьте эту секунду на количество аналогичных тестов, на количество разработчиков, на количеств запусков в день… И вы поймёте, что из-за таких таймаутов вы можете терять впустую много часов работы еженедельно, если не ежедневно.



    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?
    Загрузка тестовых данных вне блоков теста.

    На первый взгляд кажется, что всё равно, где читать тестовые данные — в блоке describe, it или в самом модуле. На второй тоже. Но представьте, что у вас сотни тестов, и во многих из них используются тяжёлые данные. Если вы их грузите вне теста, то это приведёт к тому, что все тестовые данные будут оставаться в памяти до конца выполнения тестов, и запуск со временем будет потреблять всё больше и больше оперативной памяти — пока не окажется, что тесты больше вообще не запускаются на стандартных рабочих машинах.



    5



    const {assert} = require('chai');
    const sinon = require('sinon');
    classDog{
      // 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!"');
      });
    });
    


    Was ist hier los?
    Код фактически заменён стабами.

    Наверняка вы сразу увидели эту смешную ошибку. В реальном коде это, конечно, не настолько очевидно — но я видел код, который был обвешан стабами настолько, что вообще ничего не тестировал.



    6



    const sinon = require('sinon');
    const {assert} = require('chai');
    classWidget{
      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?
    Зависимость между тестами.

    С первого взгляда понятно, что здесь забыли написать

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


    Но проблема не только в этом, а в том, что для всех тестов используется один и тот же sandbox. И очень легко таким образом смутировать среду выполнения тестов таким образом, что они начнут зависеть друг от друга. После этого тесты начнут выполняться только в определённом порядке, и вообще непонятно что тестировать.

    К счастью, sinon.sandbox в какой-то момент был объявлен устаревшим и выпилен, так что с такой проблемой вы можете столкнуться только на легаси проекте — но есть огромное множество других способов смутировать среду выполнения тестов таким образом, что потом будет мучительно больно расследовать, какой код виновен в некорректном поведении. На хабре, кстати, недавно был пост про какой-то шаблон вроде «Ice Factory» — это не панацея, но иногда помогает в таких случаях.




    7. Riesige Testdaten in der Testdatei



    Ich habe oft gesehen, wie große JSON-Dateien und sogar XML im Test stecken. Ich denke natürlich, warum es nicht wert ist, es zu tun - es wird schmerzhaft, es anzuschauen, zu bearbeiten, und jede IDE wird Ihnen kein solches Dankeschön sagen. Wenn Sie große Testdaten haben, nehmen Sie diese aus der Testdatei heraus.

    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?
    Лишние тесты.

    В данном случае разработчик был очень обеспокоен тем, что его уникальные идентификаторы будут уникальными, поэтому написал на это проверку. В целом понятное стремление — но лучше прочитать документацию или прогнать такой тест несколько раз, не добавляя его в проект. Запуск его в каждом билде не имеет никакого смысла.

    Ну и завязка на случайные значения в тесте — сама по себе является отличным способом выстрелить себе в ногу, сделав нестабильный тест на пустом месте.



    9. Mangel an Spott


    Es ist viel einfacher, Tests mit Live-Base- und Sotalnogo-Diensten auszuführen und Tests durchzuführen.
    Früher oder später wird dies jedoch der Fall sein - Datenlöschungstests werden auf der Produktbasis durchgeführt, sie fallen aufgrund eines nicht funktionierenden Partnerservices ein oder Ihr CI hat einfach keine Basis, auf der sie vertrieben werden können. Im Allgemeinen ist der Artikel ziemlich holivarny, aber in der Regel - wenn Sie externe Dienste nachahmen können, ist es besser, dies zu tun.

    11


    const {assert} = require('chai');
    classCustomErrorextendsError{
    }
    functionmytestFunction()
    {
      thrownew 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?
    Усложнённая отладка ошибок.

    Всё неплохо, но есть одна проблема — если тест вдруг упал, то вы увидите ошибку вида

    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)


    Дальше, чтобы понять, что за ошибка собственно случилась — вам придётся переписывать тест. Так что в случае неожиданной ошибки — постарайтесь, чтобы тест рассказал о ней, а не только сам факт того, что она произошла.



    12


    const {assert} = require('chai');
    functionsomeVeryBigFunc1()
    {
      return1; // imagine a tonn of code here
    }
    functionsomeVeryBigFunc2()
    {
      return2; // 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);
      });
    });
    


    Was ist hier los?
    Всё в блоке before.

    Казалось бы, классный подход сделать все операции в блоке `before`, и таким образом оставить внутри `it` только проверки.
    На самом деле нет.
    Потому что в этом случае возникает каша, в которой нельзя ни понять время реального выполнения тестов, ни причину падения, ни то, что относится к одному тесту, а что к другому.
    Так что вся работа теста (кроме стандартных инициализаций) должна выполняться внутри самого теста.



    13


    const {assert} = require('chai');
    const moment = require('moment');
    functionsomeDateBasedFunction(date)
    {
      if (moment().isAfter(date))
      {
        return0;
      }
      return1;
    }
    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?
    Завязка на даты.

    Тоже казалось бы очевидная ошибка — но тоже периодически возникает у уставших разработчиков, которые уже считают, что завтра никогда не наступит. И билд который отлично собирался вчера, внезапно падает сегодня.

    Помните, что любая дата наступит рано или поздно — так что или используйте эмуляцию времени штуками вроде `sinon.fakeTimers`, или хотя бы ставьте отдалённые даты вроде 2050 года — пускай голова болит у ваших потомков...



    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-requireconst moment = require('moment');
        const someDate = moment('2010-01-01').format('MMMM');
        assert.equal(someDate, 'January');
      });
    });


    Was ist hier los?
    Динамическая подгрузка модулей.

    Если у вас стоит Eslint, то вы наверняка уже запретили динамические зависимости. Или нет.
    Часто вижу, что разработчики стараются подгружать библиотеки или различные модули прямо внутри тестов. При этом они в целом знают, как работает `require` — но предпочитают иллюзию того, что им будто бы дадут чистый модуль, который никто пока что не смутировал.
    Такое предположение опасно тем, что загрузка дополнительных модулей во время тестов происходит медленнее, и опять же приводит к большему количеству неопределённого поведения.



    15


    functionsomeComplexFunc()
    {
      // Imagine a piece of really strange code herereturn1;
    }
    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);
      });
    });


    Was ist hier los?
    Непонятные названия тестов.

    Наверное, вы устали от очевидны вещей, да? Но всё равно придётся о ней сказать потому что многие не утруждаются написанием понятных названий для тестов — и в результате понять, что делает тот или иной тест, можно только после долгих исследований.



    16


    const {assert} = require('chai');
    constPromise = require('bluebird');
    functionsomeTomeoutingFunction()
    {
      thrownewPromise.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?
    Отсутствие проверки выброшенной ошибки.

    Часто нужно проверить, что в каком-то случае функция выкидывает ошибку. Но всегда нужно проверять, те ли это дроиды, которых мы ищем — поскольку внезапно может оказаться, что ошибка была выкинута другая, в другом месте и по другим причинам...



    17



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


    Was ist hier los?
    Отключенные тесты.

    Конечно, всегда может появиться ситуация, когда код уже много раз протестирован руками, нужно срочно его катить, а тест почему-то не работает. Например, из-за не очевидной завязки на другой тест, о которой я писал ранее. И тест отключают. И это нормально. Не нормально — не поставить мгновенно задачу на то, чтобы включить тест обратно. Если это не сделать, то количество отключенных тестов будет плодиться, а их код — постоянно устаревать. Пока не останется единственный вариант — проявить милосердие и выкинуть все эти тесты нафиг, поскольку быстрее написать их заново, чем разбираться в ошибках.



    Hier ist so eine Auswahl rausgekommen. Alle diese Tests sind gut getestet, sind aber konstruktionsbedingt gebrochen. Fügen Sie Ihre Optionen in den Kommentaren oder in dem Repository hinzu , das ich zur Erfassung solcher Fehler erstellt habe.

    Jetzt auch beliebt: