Aka rspeс, i.e. lazy variables in tests

    As the saying goes: "The forbidden fruit is sweet," so it is with me. Having tried once to write tests on RSpec, I want to have declarative BDD DSL in each language. For example, JavaScript, has analogues mocha.js, jasmine.js, etc. But no, not enough. I would like not just any descriptions or it-s, but also lazy variables, I mean subject and let.

    At first glance stupid! An inner voice shouts “Why?”, And conscience responds: “Clean code is important!” Well, simple tests are generally mega important! ”

    This is how the library for mochajs was born, which allows you to create lazy variables (aka let) and `subject`.

    For those who understand what I’m already tensed and lit up with joy, you are welcome to Github .
    To everyone else, and especially to skeptics, I propose to look under cut.

    Why is this important to anyone?


    That is why! .

    Well, now seriously


    What do they usually write in 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');
      });
     .....
    })
    

    Everything seems to be fine, the account has been created, the user has been created, the payment is rejected if less money came from the payment system than wanted ... But when you need to replace the user or create an account with other parameters, we come to a more deplorable 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');
        });
      });
     .....
    })
    


    That is, we just take a duplicate setup, pass other parameters and howl! Long live the copy-paste ... And who will clean the variables in `afterEach`?

    Laziness against mine-paste!


    One of the tasks that this library solves is the destruction of mine-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 has become smaller, copy-paste less, transparency is higher! Hurrah! Moreover, 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 name collisions. If such a variable already exists, we get an exception.

    And now about how it works


    Lazy variables are lazy ones that are created only at the moment of access to them. That is, in the last `describe`, our` $ invoice` is created inside `it` (and not` beforeEach`), but with another user: instead of the usual user, an admin is created. Thus, a substitution occurred and the accounts are now attached to our admin, who can do anything.

    Now I think it’s clear that lazy variables are created in the context of the suite, not the test, and that writing `def` inside the test is illogical (I know, we all know are smart people, but I just had to write this).

    In the end, what’s the output?


    1. Laziness! No more extra calls. Don't let tests be slow
    2. Ability to compose variables
    3. Lack of copy paste
    4. Prudent cleaning of variables after each `it`
    5. And a couple of little features in addition about which you can read at your leisure in README

    Tests in one line?


    As mentioned above, the library allows you to define `subject` for the test
    Example with 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');
      });
    


    Which in turn leads us to the 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'));
      // or even better
      it(() => isExpected.to.be.pending)
    

    This is not yet, but it’s quite simple to do with ES6 features in the sleeve and the ability to create `subject` in the 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 propose to look also at this article

    P.PS : SOLID in tests is more important than SOLID in all other places.

    Also popular now: