Jasmine DRY: а ты правильно пишешь тесты?

    В промежутке времени между переквалификацией с Back-end программиста на Front-end, мне пришлось писать код для RoR приложения (да-да и тесты были). Интересным для меня показалась своеобразная атмосфера сообщества рубистов, которые очень строго относятся к написанию кода и если ты пишешь плохо, то тебе могут поломать пальцы не простить. Ведь код должен быть максимально простым и читабельным.

    Это же правило применимо и к тестам (как по мне то, они должны быть на порядок проще чем сам код). В дополнение, в тестах есть свое золотое правило — One Expectation per Test. Не нужно писать кучу expect/assert/should вызовов в одном тесте, просто перестаньте это делать! И не забывайте, что тесты это тоже код, а copy-paste — плохая практика.

    Что такое плохой тест


    Разбираясь в 3.0 версии Knockout.js, я решил посмотреть тесты в надежде найти хоть какое-то упоминание о новом свойстве after внутри байндингов. Честно говоря, меня возмутила сложность написанных тестов.

    Плохой тест
    describe('Binding: Checked', function() {
        beforeEach(jasmine.prepareTestNode);
    
        it('Triggering a click should toggle a checkbox\'s checked state before the event handler fires', function() {
            testNode.innerHTML = "<input type='checkbox' />";
            var clickHandlerFireCount = 0, expectedCheckedStateInHandler;
            ko.utils.registerEventHandler(testNode.childNodes[0], "click", function() {
                clickHandlerFireCount++;
                expect(testNode.childNodes[0].checked).toEqual(expectedCheckedStateInHandler);
            })
            expect(testNode.childNodes[0].checked).toEqual(false);
            expectedCheckedStateInHandler = true;
            ko.utils.triggerEvent(testNode.childNodes[0], "click");
            expect(testNode.childNodes[0].checked).toEqual(true);
            expect(clickHandlerFireCount).toEqual(1);
    
            expectedCheckedStateInHandler = false;
            ko.utils.triggerEvent(testNode.childNodes[0], "click");
            expect(testNode.childNodes[0].checked).toEqual(false);
            expect(clickHandlerFireCount).toEqual(2);
        });
    });
    


    Если не учитывать, что все директивы (describe и it) являются частью спеки, то потом невозможно понять смысл теста из заголовка (it triggering a click should...). Получается ведь бред, как в заголовке так и в самом тесте.

    Вот список вопросов, которые помогают мне создавать понятные и простые спеки:
    1. Какие тестовые данные?
    2. Какой контекст тестирования?
    3. Какие кейсы нужно покрыть?
    4. Как можно сгруппировать эти кейсы?

    Для выше приведенного примера:
    1. Поле ввода checkbox
    2. Пользователь жмет на checkbox
    3. Кейсы:
      1. Состояние меняется до вызова обработчика клика
      2. Состояние меняется в отмеченный, если checkbox был не отмечен
      3. Состояние меняется в не отмеченный, если checkbox был отмечен


    Теперь все то же самое только на английском:

    Просто читаемый тест
    describe('Binding: Checked', function() {
        beforeEach(jasmine.prepareTestNode);
    
        describe("when user clicks on checkbox", function () {
            beforeEach(function () {
                testNode.innerHTML = "<input type='checkbox' />";
                this.checkbox = testNode.childNodes[0];
                this.stateHandler = jasmine.createSpy("checked handler");
    
                this.checkbox.checked = false;
                ko.utils.registerEventHandler(this.checkbox, "click", function() {
                    this.stateHandler(this.checkbox.checked);
                }.bind(this));
                ko.utils.triggerEvent(this.checkbox, "click");
            })
    
            it ("changes state before event handler is triggered", function () {
                expect(this.stateHandler).toHaveBeenCalledWith(true);
            })
    
            it ("marks checkbox if it's not marked", function () {
                expect(this.checkbox.checked).toBe(true)
            })
    
            it ("unmarks checkbox if it's marked", function () {
                this.checkbox.checked = true;
                ko.utils.triggerEvent(this.checkbox, "click");
                expect(this.checkbox.checked).toBe(false);
            })
        })
    })
    


    Setup — сложный, тесты — простые. Идеальный вариант — это тест в котором находится один вызов ф-ции expect.

    Меньше кода, больше тестов


    При первом знакомстве с Jasmine я понимал, что она не идеальна, но не найдя возможности создания групповых спек, я в панике бросился за помощью в Google. К моему большому разочарованию он тоже не знал ответа, который бы меня устроил. Пришлось самому покопаться в темных недрах Jasmine и найти решение.

    Давайте представим, что существует JavaScript++, в котором есть 2 класса Array и Set с общим интерфейсом (size и contains). Теперь нужно покрыть их тестами, не дублируя код! Определим общие тесты для наших коллекций:
    sharedExamplesFor("collection", function () {
      beforeEach(function () {
         this.sourceItems = [1,2,3];
         this.collection = new this.describedClass(this.sourceItems);
      })
    
      it ("returns proper size", function () {
        expect(this.collection.size()).toBe(this.sourceItems.length);
      })
    
      // another specs
    
      it ("returns true if contains item", function () {
        expect(this.collection.contains(this.sourceItems[0])).toBe(true);
      })
    })
    

    По аналогии к Rspec, хотелось бы иметь возможность подключать спеки при помощи одного из методов:
    • itBehavesLike — выполняет тесты во вложенном контексте
    • itShouldBehaveLike — выполняет тесты во вложенном контексте
    • includeExamples — выполняет тесты в текущем контексте
    • includeExamplesFor — выполняет тесты в текущем контексте

    Note: itShouldBehaveLike и includeExamplesFor — существуют только для улучшения читаемость тестов

    // array_spec.js
    describe("Array", function () {
       beforeEach(function () {
          this.describedClass = Array;
       })
    
       itBehavesLike("collection");
       //another specs
    })
    
    // set_spec.js
    describe("Set", function () {
       beforeEach(function () {
          this.describedClass = Set;
       })
    
       itBehavesLike("collection");
       //another specs
    });
    

    Еще я обычно создаю ф-цию context (элиас для describe) для улучшения читабельности спек.

    Исходный код реализации shared spec
      // spec_helper.js
      var sharedExamples = {};
    
      window.sharedExamplesFor = function (name, executor) {
         sharedExamples[name] = executor;
      };
    
      window.itBehavesLike = function (sharedExampleName) {
          jasmine.getEnv().describe("behaves like " + sharedExampleName, sharedExamples[sharedExampleName]);
      };
    
      window.includeExamplesFor = function (sharedExampleName) {
          var suite = jasmine.getEnv().currentSuite;
          sharedExamples[sharedExampleName].call(suite);
      };
    
      window.context = window.describe;
      window.includeExamples = window.includeExamplesFor;
      window.itShouldBehaveLike = window.itBehavesLike;
    

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

      –3
      Похоже на ужасный машинный перевод.
        0
        Недавно заинтересовался возможностью авто-тестирования JS'а и хочу понять как оно таки работает. Ведь по факту тест это «зеркало» кода, т.е трудозатраты возрастают раза в полтора, и иногда это всёравно может не дать нужный эффект(функции можно протестировать, а вот увидит ли пользователь то что задумывалось — уже сложнее). Знаю что есть selenium который по факту просто кликер.
        Если кому не сложно — посоветуйте литературу и/или обзор библиотек для авто-тестирование JS. Больше всего интересует возможность тестирования backbone.
          +2
          Могу посоветовать Backbone.js Testing by Ryan Roemer (http://backbone-testing.com/). Там правда ни слова о jasmine, используются следующие библиотеки для тестирования:
          — Mocha
          — Chai
          — Sinon.JS

          Автору спасибо за статью. На самом деле очень полезно писать «понятные» тесты особенно под javascript.
          –1
          > Идеальный вариант — это тест в котором находится один вызов ф-ции expect
          Чего ж один — давайте сразу ноль.
          Насколько примитивными должны быть фловы в приложении чтоб их можно было протестировать одним ассертом?
          Во сколько раз возрастет количество тестов если сложный флоу разбить на 10-15 одиночных тестов, и не является это само по себе усложнением, замаскированным под упрощение?
            0
            Да, именно. Ведь когда Вам дают, например, интернет-магазин на разработку, то Вы начинаете дробить все на модули, модели, контроллеры, вьюхи, фичи и прочее. Это ведь упрощает общее понимание картины, не так ли? Почему бы не делать то же самое в тестах?
              0
              Конечно, но я не считаю контроллер «плохим» если в нем больше скольки-то там методов, или вьюху «плохой» потому что на ней больше 1-го поля ввода, например.
                +1
                А я считаю контроллер плохим, если в нем экшен больше 20-30 строк кода. Скажу даже больше, я считаю любой метод плохим, который называется плохо или если метод принимает больше 3 аргументов, или если у метода есть статические зависимости и таких «если» целая куча.

                Все конечно же относительно, но есть так называемые «Best Practices», которые помогают делать код лучше. Следовать им или нет — это уже другой вопрос.
                  0
                  Ограничение кол-ва параметров в методе не имеет ничего общего с best practices.
              0
              Сложный флоу в unit-тесте? А как Вы его описываете в it('...')?
              По мне так критерий должен быть примерно таким — можешь кратко сформулировать в it, что этот тест проверяет — не принципиально, будет ли там 1 expect или 5. Не можешь — надо разбить.

              А в целом, на мой взгляд, использование Arrange Act Assert (AAA) Pattern полезнее правила «1 expect».
                0
                AAA не противоречит «1 expect», поэтому просто не может быть лучше или хуже:
                1. Arrange — для этого и существуют методы before/after/around each и их аналоги (setup, teardown, etc.).
                2. Act — не принципиально где вызывать, в setUp-e или в самом тесте, зависит от ситуации.
                3. Assert — ожидаемый результат, пишется только в it.

                К-во expect-ов — это же часть SOLID. Один тест тестирует, что-то одно. При этом же документация, которая потом создается на базе тестов получается на порядок качественней и детальней.
                  0
                  А где я сказал, что оно противоречит? Я сказал «полезнее».

                  > 1. Arrange — для этого и существуют методы before/after/around each и их аналоги (setup, teardown, etc.).

                  Вы на каждый тест еще свой describe и beforeEach предлагаете создавать? При чем тут after вообще не очень понял. По мне так в некоторых случаях читабельность ухудшится.

                  > 2. Act — не принципиально где вызывать, в setUp-e или в самом тесте, зависит от ситуации.

                  Да можно, конечно, кто мешает. Паттерн то существует для повышения структурированности и читабельности, а не для маниакального написания тестов именно так, как в одном примере показано.

                  > К-во expect-ов — это же часть SOLID. Один тест тестирует, что-то одно.

                  Одно с каким уровнем детализации?

                  В целом, как то складывается ощущение, что Вы хотите поделить мир на черное и белое. У Вас серьезно на каждый тест свой describe и beforeEach? И на тестовой базе хотя бы в 1000 тестов везде один expect? Было бы интересно на такое посмотреть вместе с покрытием, а вдруг действительно хорошо выглядит. Нету ничего в открытом доступе?
              +1
              А какие у Jasmine DRY плюсы и минусы по сравнению с аналогами?

              Only users with full accounts can post comments. Log in, please.