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` внутри теста нелогично (знаю, знаю все мы умные люди, но я просто должен был это написать).
В конце концов, что на выходе?
- Ленивость! Больше никаких лишних вызовов. Не позволяем тестам быть медленными
- Возможность компонировать переменные
- Отсутствие копи-паста
- Предусмотрительную очистку переменных после каждого `it`
- И еще парочку маленьких фич в придачу о которых можно почитать на досуге в README
Тесты в одну строчку?
Как уже выше было упомянуто, библиотека позволяет определять `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 во всех других местах.