Skip to content

Instantly share code, notes, and snippets.

@traviskaufman
Last active September 19, 2022 14:35
Star You must be signed in to star a gist
Save traviskaufman/11131303 to your computer and use it in GitHub Desktop.
Better Jasmine Tests With `this`

Better Jasmine Tests With this

On the Refinery29 Mobile Web Team, codenamed "Bicycle", all of our unit tests are written using Jasmine, an awesome BDD library written by Pivotal Labs. We recently switched how we set up data for tests from declaring and assigning to closures, to assigning properties to each test case's this object, and we've seen some awesome benefits from doing such.

The old way

Up until recently, a typical unit test for us looked something like this:

describe('views.Card', function() {
  'use strict';
  
  var model, view;
  
  beforeEach(function() {
    model = {};
    view = new CardView(model);
  });
  
  describe('.render', function() {
    beforeEach(function() {
      model.title = 'An Article';
      view.render();
    });
    
    it('creates a "cardTitle" h3 element set to the model\'s title', function() {
      expect(view.$el.find('.cardTitle')).toContainText(model.title);
    });
    
    describe('when the model card type is "author_card"', function() {
      beforeEach(function() {
        model.type = 'author_card';
        view.render();
      });
      
      it('adds an "authorCard" class to its $el', function() {
        expect(view.$el).toHaveClass('authorCard');
      });
    });
  });
  
  // ...
});

We set up state by declaring variables within a describe block's lexical scope. We then assign/modify them as necessary in subsequent beforeEach/afterEach statements.

We noticed, however, that as our tests became more complex, this pattern became increasingly difficult. We found ourselves jumping around spec files, trying to find out where a given variable was initially defined. We also ran into subtle bugs due to clobbering variables with common names (i.e. model, view) within a given scope, failing to realize they had already been defined. Furthermore, our declaration statements in describe blocks started looking something like

var firstVar, secondVar, thirdVar, fourthVar, fifthVar, ..., nthVar

Which was ugly and hard to parse. Finally, we would sometimes run into flaky tests due to "leaks" - test-specific variables that were not properly cleaned up after each case.

The new, better way

For every test (and their beforeEach/afterEach hooks), jasmine sets the receiver of each function to an initially empty object. This object, which is called userContext within Jasmine's source code, can have properties assigned to it, and gets blown away at the end of each test. In an attempt to address the issues we were having, we recently switched over to assigning variables to this object, rather than declaring them within describe and then assigning them. So our original code above now looked something like this:

describe('views.Card', function() {
  'use strict';
  
  beforeEach(function() {
    this.model = {};
    this.view = new CardView(this.model);
  });
  
  describe('.render', function() {
    beforeEach(function() {
      this.model.title = 'An Article';
      this.view.render();
    });
    
    it('creates a "cardTitle" h3 element set to the model\'s title', function() {
      expect(this.view.$el.find('.cardTitle')).toContainText(this.model.title);
    });
    
    describe('when the model card type is "author_card"', function() {
      beforeEach(function() {
        this.model.type = 'author_card';
        this.view.render();
      });
      
      it('adds an "authorCard" class to its $el', function() {
        expect(this.view.$el).toHaveClass('authorCard');
      });
    });
  });
  
  // ...
});

Why the new way rocks

Switching over to this pattern has yielded a significant amount of benefits for us, including:

No more global leaks

Because jasmine instantiates a new userContext object for each test case, we didn't have to worry about test pollution any more. This helped ensure isolation between our tests, making them a lot more reliable.

Clear meaning

Every time we see a this.<PROP> reference in our tests, we know that it refers to data set up in a beforeEach/afterEach statement. That, coupled with removing exhaustive var declarations in describe blocks, have made even our largest tests clear and understandable.

Improved code reuse via dynamic invocation.

One of the best things about javascript is its facilities for dynamic invocation. Because we now use the userContext object to store all of our variables, and jasmine dynamically invokes each test function and hook with this context, we could begin to refactor our boilerplate code into helper functions that could be reused across specs.

Consider the initial beforeEach in our test case above:

beforeEach(function() {
  this.model = {};
  this.view = new CardView(this.model);
});

Chances are we're going to want something like this in most of our view specs: a blank model and a view instantiated with the model as its argument. We can turn this logic into a reusable function like so:

// spec_helper.js
beforeEach(function() {
  this.setupViewModel = function setupViewModel(ViewCtor) {
    this.model = {};
    this.view = new ViewCtor(this.model);
  };
});

Now, in any of our view tests, we could just write the following:

beforeEach(function() {
  this.setupViewModel(SomeView);
});

And we'd be good to go!

Reduced Code Duplication via Lazy Evaluation

This is where things get really awesome.

It's unfortunate in our original spec that we have to call render() twice. We call it once in the top-level beforeEach, and then again when we change the model's state in the nested describe(). We could call render directly within the tests, but we'd rather keep all business logic within beforeEach statements, making only assertions within tests themselves. RSpec has this really nice let() helper method which will lazily evaluate a variable. This way even if a variable relies on some context, it need not have to be declared more than once.

Thankfully, because we're now assigning our variables to this, we can take advantage of ES5 accessor properties to implement this lazy evaluation in our javascript specs!! Take a look at how our new render test cases would look when leveraging this:

describe('.render', function() {
  beforeEach(function() {
    var _renderedView;
    this.model.title = 'An Article';
    // When we access this property, it will call `render()` on `this.view` and give 
    // us a fresh, updated copy per test. No code duplication FTW!!
    Object.defineProperty(this, 'renderedView', {
      get: function() {
        if (!_renderedView) {
          _renderedView = this.view.render();
        }
        return _renderedView;
      },
      set: function() {}, //NOP
      enumerable: true,
      configurable: true
    });
  });
  
  it('creates a "cardTitle" h3 element set to the model\'s title', function() {
    expect(this.renderedView.$el.find('.cardTitle')).toContainText(this.model.title);
  });
  
  describe('when the model card type is "author_card"', function() {
    beforeEach(function() {
      this.model.type = 'author_card';
      // No need to re-render the view here!
    });
    
    it('adds an "authorCard" class to its $el', function() {
      expect(this.renderedView.$el).toHaveClass('authorCard');
    });
  });
});

Taking this a step further, we can genericize this within a spec helper file:

// spec_helper.js
beforeEach(function() {
  // need to use "_" suffix since 'let' is a token in ES6
  this.let_ = function(propName, getter) {
    var _lazy;
    
    Object.defineProperty(this, propName, {
      get: function() {
        if (!_lazy) {
          _lazy = getter.call(this);
        }
        
        return _lazy;
      },
      set: function() {},
      enumerable: true,
      configurable: true
    });
  };
});

Now in your specs:

describe('.render', function() {
  beforeEach(function() {
    this.let_('renderedView', function() {
      return this.view.render();
    });
    this.model.title = 'An Article';
  });
  
  // ...
});

Pretty slick if you ask us. However, it is important to be judicious when doing things such as this. The method above uses a fair amount of black magic, and can lead to confusion if your team members aren't that familiar with newer ES5 concepts. However, taking advantage of these mechanisms in a smart manner can lead to a lot of reduced code duplication and therefore much higher test maintainability. We'd consider that a win for any team!

@scamden
Copy link

scamden commented Nov 19, 2014

dude this post is a gold mine. you have made my testing life like 10x better

@vbasky
Copy link

vbasky commented Feb 9, 2015

You have made using Jasmine in my life a lot better.

@Keysox
Copy link

Keysox commented Aug 12, 2015

Thank you for sharing this excellent post! Setup and Teardown will now be much easier using 'this'.

@awilkening
Copy link

Appreciate the write up!

@lotemh
Copy link

lotemh commented Nov 10, 2015

Very nice :)
In your lazy evaluation example, why is the _renderedView a var and not on this?

@amypellegrini
Copy link

Seems like the way to go to avoid code duplication.

@yoda-yoda
Copy link

Nice post

@munepom
Copy link

munepom commented Dec 13, 2016

Nice 👍

@schmuli
Copy link

schmuli commented Jan 10, 2017

I know it has been a while since you posted this, however it is still relevant and a brilliant way to manage tests.

I was wondering if you ever used TypeScript, and specifically if you used this pattern when writing tests with TypeScript?

@brightgarden
Copy link

brightgarden commented Apr 25, 2017

@schmuli, I have used this pattern using Typescript on an Angular (now 4.0.0) project, and it is very nice. If you are on Typescript 2.0 or higher, you can do the following in your spec files:

interface ThisContext {
  myClass: MyClassType,
  results: MyResultsType
}

describe('myClassTests', function(){

  beforeEach(function(this:ThisContext){ 
    this.myClass = new MyClassType();
  });

  it('some test', function(this:ThisContext){
    // act
    this.results = this.myClass.doSomething();

    // assert
    expect(this.results.someValue()).toBeTruthy(); // or whatever
  });

});

See Updated Typescript Functions Documentation and Typescript 2.0 Release Notes for more details about specifying the type of this.

@4leem
Copy link

4leem commented Sep 8, 2017

Fantastic! Thanks for sharing. Nice post.

@stalniy
Copy link

stalniy commented Jan 9, 2018

Few years ago I created a library for Mocha https://github.com/stalniy/bdd-lazy-var which does exactly what you want guys :) but in my opinion provides clearer interface. Currently I'm in process of finishing support for jasmine2 and jest :) Today plan to publish 2.0 version

Update: @traviskaufman I will use some of your description in my README file if you don't mind because it appeared you were able to describe benefits/downsides of using lazy vars much better then I did few years ago :)

@dlidstrom
Copy link

It is unclear to me why var _renderedView; can be safely put directly in the before_each function instead of on this? Eg here:

describe('.render', function() {
  beforeEach(function() {
    var _renderedView; <---- ?

@elliot-nelson
Copy link

@dlidstrom It's safe because any function defined within that function will always have a reference to that variable. If you were to take jasmine out of the equation, it's basically the same thing as this:

    function newCounter() {
        var index = 0;
        return { get value() { return ++index; } };
    }

    var a = newCounter();
    var b = newCounter();

    // The "getter" for value is a function, and a function always retains access
    // to its local scope (in this case, each getter retains access to its own unique
    // 'index' variable...)
    console.log(a.value);    // => 1
    console.log(a.value);    // => 2
    console.log(a.value);    // => 3
    console.log(b.value);    // => 1

    // Even though those 'index' variables are essentially private, and can't be accessed
    // outside that function's scope.
    console.log(a.index);    // => undefined

The beauty of this construct for tests in jasmine is that even if you introduce a drastic mistake in your unit test, you can't pollute the next test. For example: maybe all of your unit tests are async promise-based tests, and you accidentally introduce an error where chunks of your logic execute after the test finishes, maybe due to bad chaining. Because each test gets its own this context, just like the newCounter() example above, the next test can start running safely even though the prior test may still be manipulating the prior this context. This assurance is impossible with the old block-level "vars at the top of my describe" approach.

@navneetnagpal
Copy link

Nice article and discussion very helpful!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment