June 21, 2013

Classes
Tags blog

Unit Testing Ember Data Models

<p><a href=http://emberjs.com/>Ember.js</a> is a JavaScript <a href=http://en.wikipedia.org/wiki/Model%E2%80%93view%E

Ember.js is a JavaScript MVC framework for creating ambitious web applications and is one that, along with the Ember Data library's models, SoftLayer uses for its projects. The "M" in MVC stands for model which is the most important layer in an application, for if the data models are not correctly architected anything built on top of them will inherit their deficiencies. While this post is not going to discuss how to architect your data models in Ember.js it is going to propose a way in which you can Unit Test your model's definitions so that you keep any incongruent changes from being introduced into them.

When we write our Unit Tests we want them to be able to completely test the definition of our data models. This includes properties, their type, any relationships they have with other data models, the generated result of any computed properties and the properties computed properties are observing. Take, for example, the following model definition:

App.User = DS.Model.extend({
    firstName: DS.attr( 'string' ),
    lastName:  DS.attr( 'string' ),
    account:   DS.belongsTo( 'App.Account' ),
 
    fullName: function() {
        return this.get( 'firstName' ) + ' ' + this.get( 'lastName' );
    }.property( 'firstName', 'lastName' )
});

Let's start with simply checking that the properties we expect to exist in our model definition in fact actually do. We use Mocha.js, accompanied by Chai.js, to perform our Unit Tests, which is the syntax you see below:

describe( 'User Model', function() {
    it( 'property: `firstName`', function() {
        var property = App.User.metaForProperty( 'firstName' );
    });
 
    it( 'property: `lastName`', function() {
        var property = App.User.metaForProperty( 'lastName' );
    });
 
    it( 'property: `account`', function() {
        var property = App.User.metaForProperty( 'account' );
    });
});

You may notice that we haven't written any test conditions to test for the properties existence. This is because in order to get the meta data of the model we're testing you have to first call the metaForProperty() method before we could even write any test conditions. If the call to metaForProperty() fails due to the property not existing then our test will fail and we have achieved what we set out to do. We could write a test condition that specifically tests for the property name, such as like:

it( 'property: `firstName`', function() {
    var property = App.User.metaForProperty( 'firstName' );
    expect( property.name ).to.equal( 'firstName' );
});

but I hope you can see how this is redundant. We do want to check for other properties of each individual property though, such as its type and whether or not Ember sees the property as an attribute on the model. The following adds these tests in:

describe( 'User Model', function() {
    it( 'property: `firstName`', function() {
        var property = App.User.metaForProperty( 'firstName' );
        expect( property.type ).to.equal( 'string' );
        expect( property.isAttribute ).to.equal( true, 'Expected attribute' );
    });
 
    it( 'property: `lastName`', function() {
        var property = App.User.metaForProperty( 'lastName' );
        expect( property.type ).to.equal( 'string' );
        expect( property.isAttribute ).to.equal( true, 'Expected attribute' );
    });
});

The 'account' property on our model is not what Ember considers to be an attribute but is rather a relationship. We can setup our tests for this as follows:

it( 'property: `account`', function() {
    var property = App.User.metaForProperty( 'account' );
    expect( property.type ).to.equal( 'App.Account' );
    expect( property.isRelationship ).to.equal( true, 'Expected relationship' );
    expect( property.kind ).to.equal( 'belongsTo' );
});

The 'fullName' property is a computed property and it should be tested a little differently than the other properties we have tested so far. A computed property can observe other properties so we need to test that this is configured correctly. Also, up to this point we have been able to test everything about the model without any need for any mock data, relying solely on the metadata information Ember is able to provide about our model. Most of your computed properties will likely act upon actual data so we need mock data to be able to accurately test our computed properties function. An example of how to do this is below:

it( 'computed property: `fullName`', function() {
    expect( [ 'firstName', 'lastName' ] ).to.eql( Ember.meta( App.User.proto() ).descs[ 'fullName' ]._dependentKeys );
 
    var store = DS.Store.create({
        revision: 12,
        adapter: DS.Adapter.create()
    });
 
    store.load( App.User, {
        id:        1,
        firstName: 'Jeremy',
        lastName:  'Brown'
    });
 
    var user = store.find( App.User, 1 );
 
    expect( user.get( 'fullName' ) ).to.be.a( 'string' );
    user.get( 'fullName' ).should.equal( 'Jeremy Brown' );
});

So there you have it – an approach to unit testing Ember Data models that does not require mock data (except for where required) and that tests everything we need to: properties, their type, any relationships they have with other data models, the generated result of any computed properties and the properties computed properties are observing. I leave you with a final code example below that puts all of these examples together into one and happy testing!

describe( 'User Model', function() {
    it( 'property: `firstName`', function() {
        var property = App.User.metaForProperty( 'firstName' );
        expect( property.type ).to.equal( 'string' );
        expect( property.isAttribute ).to.equal( true, 'Expected attribute' );
    });
 
    it( 'property: `lastName`', function() {
        var property = App.User.metaForProperty( 'lastName' );
        expect( property.type ).to.equal( 'string' );
        expect( property.isAttribute ).to.equal( true, 'Expected attribute' );
    });
 
    it( 'property: `account`', function() {
        var property = App.User.metaForProperty( 'account' );
        expect( property.type ).to.equal( 'App.Account' );
        expect( property.isRelationship ).to.equal( true, 'Expected relationship' );
        expect( property.kind ).to.equal( 'belongsTo' );
    });
 
    it( 'computed property: `fullName`', function() {
        expect( [ 'firstName', 'lastName' ] ).to.eql( Ember.meta( App.User.proto() ).descs[ 'fullName' ]._dependentKeys );
 
        var store = DS.Store.create({
            revision: 12,
            adapter: DS.Adapter.create()
        });
 
        store.load( App.User, {
            id:        1,
            firstName: 'Jeremy',
            lastName:  'Brown'
        });
 
        var user = store.find( App.User, 1 );
 
        expect( user.get( 'fullName' ) ).to.be.a( 'string' );
        user.get( 'fullName' ).should.equal( 'Jeremy Brown' );
    });
});

-Jeremy