Aka rspeс, т.е. ленивые переменные в тестах

    Как говорится: «Запретный плод сладок», так и у меня. Попробовав однажды писать тесты на RSpec, хочется иметь декларативный BDD DSL в каждом языке. Вот например JavaScript, имеет аналоги mocha.js, jasmine.js, etc. Но нет, мало. Хочется не просто всяких describe-ов или it-ов, а еще и ленивых переменных, я имею в виду subject и let.

    На первый взгляд глупо! Внутренний голос кричит «Зачем?», а совесть в ответ: «Чистый код — это важно! Ну а простые тесты — вообще мега важно!».

    Вот так и родилась библиотека для mochajs, которая позволяет создавать ленивые переменные (aka let) и `subject`.

    Для тех кто понимает о чем я и уже напрягся засветился от радости, милости просим на Github.
    Всем остальным, а в особенности скептикам предлагаю заглянуть под cut.

    Почему это вообще кому-то важно?


    Вот почему!.

    Ну а теперь серьёзно


    Что обычно пишут в тестах?
    describe('Invoice', function() {
      var invoice, user;
    
      beforeEach(function() {
        user = User.create({ role: 'member' });
        invoice = user.invoices.create({ price: 10, currency: 'USD' });
      });
    
      it('has status "fraud" if amount does not equal to invoice amount', function() {
          invoice.paid(1, 'USD');
          expect(invoice.status).to.equal('fraud');
      });
    
      it('has status "fraud" if currency does not equal to invoice currency', function() {
          invoice.paid(10, 'ZWD');
          expect(invoice.status).to.equal('fraud');
      });
     .....
    })
    

    Вроде бы все отлично, счет создан, пользователь создан, оплата отклоняется если от платежной системы пришло меньше денег, чем хотелось… Но когда нужно подменить пользователя или создать счет с другими параметрами мы приходим к более плачевному варианту
    Осторожно тесты!
    describe('Invoice', function() {
      var invoice, user;
    
      describe('by default', function() {
        beforeEach(function() {
          user = User.create({ role: 'member' });
          invoice = user.invoices.create({ price: 10, currency: 'USD' });
        });
    
        it('has status "fraud" if amount does not equal to invoice amount', function() {
            invoice.paid(1, 'USD');
            expect(invoice.status).to.equal('fraud');
        });
    
        it('has status "fraud" if currency does not equal to invoice currency', function() {
            invoice.paid(10, 'ZWD');
            expect(invoice.status).to.equal('fraud');
        });
      });
    
      describe('when user is admin', function() {
        beforeEach(function() {
          user = User.create({ role: 'admin' });
          invoice = user.invoices.create({ price: 10, currency: 'USD' });
        });
    
        it('has status "paid" if amount does not equal to invoice amount', function() {
            invoice.paid(1, 'USD');
            expect(invoice.status).to.equal('paid');
        });
      });
     .....
    })
    


    Т.е., просто берем дублируем setup, передаем другие параметры и воуля! Да здравствует копи-паст… А переменные кто будет чистить в `afterEach`?

    Лень против копи-паста!


    Одна из задач которую решает эта библиотечка — это уничтожение копи-паста! Как именно? Да просто
    describe('Invoice', function() {
      def('user', function() {
        return User.create({ role: 'member' });
      });
    
      def('invoice', function() {
        return $user.invoices.create({ price: 10, currency: 'USD' });
      });
    
      describe('by default', function() {
        it('has status "fraud" if amount does not equal to invoice amount', function() {
            $invoice.paid(1, 'USD');
            expect($invoice.status).to.equal('fraud');
        });
      });
    
      describe('when user is admin', function() {
        def('user', function() {
          return User.create({ role: 'admin' });
        });
          
        it('has status "paid" if amount does not equal to invoice amount', function() {
            $invoice.paid(1, 'USD');
            expect($invoice.status).to.equal('paid');
        });
      });
     .....
    })
    

    Кода стало меньше, копи-пасты меньше, прозрачность выше! Ура! Мало того, переменные удаляются после каждого теста самостоятельно и не нужно писать `afterEach` блоки. Удобно?

    Note: знак `$` к переменным добавлен во избежание коллизий с именами. Если такая переменная уже существует — получаем exception.

    А теперь о том как это работает


    Ленивые переменные на то и ленивые, что создаются только в момент доступа к ним. Т.е., в последнем `describe` наш `$invoice` создается внутри `it` (а не `beforeEach`), но уже с другим пользователем: вместо обычного создается админ. Таким образом произошла подмена и счета теперь привязываются к нашему админу, который может творить все, что угодно.

    Теперь думаю понятно, что ленивые переменные создаются в контексте suite-а, а не теста и что писать `def` внутри теста нелогично (знаю, знаю все мы умные люди, но я просто должен был это написать).

    В конце концов, что на выходе?


    1. Ленивость! Больше никаких лишних вызовов. Не позволяем тестам быть медленными
    2. Возможность компонировать переменные
    3. Отсутствие копи-паста
    4. Предусмотрительную очистку переменных после каждого `it`
    5. И еще парочку маленьких фич в придачу о которых можно почитать на досуге в README

    Тесты в одну строчку?


    Как уже выше было упомянуто, библиотека позволяет определять `subject` для теста
    Пример с subject
    describe('Invoice', function() {
      subject(function() {
        var admin = User.create({ role: 'admin' });
    
        return Invoice.create({ price: 10, currency: 'USD', user: admin })
      });
    
      it('has status "pending" by default', function() {
        expect($subject.status).to.equal('pending');
      });
    


    Что в свою очередь приводит нас к синтаксису
    describe('Invoice', function() {
      subject(function() {
        var admin = User.create({ role: 'admin' });
    
        return Invoice.create({ price: 10, currency: 'USD', user: admin })
      });
    
      its('status', () => isExpected.to.equal('pending'));
    
      // or even better
    
      it(() => isExpected.to.be.pending)
    

    Этого пока нет, но достаточно просто сделать имея ES6 фичи в рукаве и возможность создавать `subject` в тестах.

    Update: думаю в случае использования `chai`, лучше писать как-то так
      its('status', () => is.expected.to.equal('pending'));
    


    P.S.: для тех кому не хватает `sharedExamples` в JavaScript тестах предлагаю посмотреть еще и эту статью

    P.P.S.: SOLID в тестах важнее SOLID во всех других местах.

    Similar posts

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

    More
    Ads

    Comments 2

      0
      Отлично. Только на днях появилась необходимость писать тесты на node.js (до этого как раз использовал rspec в rails). Обязательно использую эту библиотеку.
        0
        Кому интересны детали милости, просим на medium.com/@sergiy.stotskiy/lazy-variables-with-mocha-js-d6063503104c#.suvytqfmm

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