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` в тестах.

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

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

ссылка на оригинал статьи https://habrahabr.ru/post/114047/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *