As the saying goes: “The Forbidden Fruit is Sweet,” so it is with me. Having tried writing tests for RSpec once, I want to have a declarative BDD DSL in each language. For example, JavaScript, has analogs mocha.js, jasmine.js, etc. But no, not enough. It would be desirable not just any reports or it, but also lazy variables, I mean subject and let.
At first glance, stupid! The inner voice shouts “Why?”, And conscience responds: “Clean code is important! Well, simple tests are generally important! ”
This is how the mochajs library was born, which allows you to create lazy variables (aka let) and `subject`.
')
For those who understand what I’m about and already
tensed up, lit up with joy, you are welcome on
Github .
All the rest, and especially skeptics, I suggest to look under the cut.
Why is this important to anyone?
That is
why! .
Well, now seriously
What is usually written in the tests?
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'); }); ..... })
It seems that everything is fine, the account has been created, the user has been created, the payment deviates if less money came from the payment system than we wanted ... But when it is necessary to change the user or create an account with other parameters, we come to a more pitiable option
Caution tests! 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'); }); }); ..... })
Ie, we just take the duplicate setup, pass on other parameters and voila! Long live copy-pastes ... And who will clean the variables in `afterEach`?
Laziness against copy-paste!
One of the tasks that this library solves is the destruction of the copy-paste! How exactly? Yes, just
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'); }); }); ..... })
The code is less, save-paste less, transparency is higher! Hooray! Moreover, the variables are deleted after each test independently and there is no need to write `afterEach` blocks. Conveniently?
Note : The `$` sign has been added to variables to avoid collisions with names. If such a variable already exists, we get exception.
And now about how it works.
Lazy variables are so lazy that they are created only when they are accessed. That is, in the last `describe` our` $ invoice` is created inside `it` (and not` beforeEach`), but with a different user: instead of the usual, an admin is created. Thus, a substitution occurred and the accounts are now tied to our admin, who can do anything.
Now I think it is clear that lazy variables are created in the context of a suite, rather than a test, and that writing `def` inside the test is illogical (I know, we all know are smart people, but I just had to write it).
In the end what is the output?
- Laziness! No more extra calls. Don't let tests be slow.
- The ability to compose variables
- Lack of copy-paste
- Provisional clearing of variables after each `it`
- And a couple of little features in addition to which you can read at leisure in the README
Tests in one line?
As mentioned above, the library allows you to define the subject for the test.
Subject example 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'); });
Which in turn leads us to syntax
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'));
This is not there yet, but it is enough just to have ES6 features in the sleeve and the ability to create `subject` in tests.
Update: I think in the case of using `chai`, it is better to write something like this
its('status', () => is.expected.to.equal('pending'));
PS : for those who do not have enough `sharedExamples` in JavaScript tests I suggest to look also
this articlePPS : SOLID in tests is more important than SOLID in all other places.