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

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

    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 во всех других местах.

© Habrahabr.ru