Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Testing Ember Services that Fetch Data

I have an ember service thats primary concern is to fetch data for a specific model and the descendants of the model. The reason I am using this in a service is because the route for this particular type is using a slug which is not the primary key and therefore needs to do a store.query instead of store.find. When we fetch this model I have some logic that peeks the ember store to see if we can load it from there before going to the api query. Also this vendor is watching for the slug change and updating the current model based on that.

The problem I am having is that this seems to have very little documentation when it comes to how to test a thing like this. In fact I don't see a section on testing services anywhere in the guides here http://guides.emberjs.com/v2.1.0/

This is a snippet of the service in question.

import Ember from 'ember';

export default Ember.Service.extend({
    _vendorSlug: null,
    vendor: null,

    vendorSlug: function (key, value) {
        if (arguments.length > 1) {
            if (this._vendorSlug) {
                return this._vendorSlug;
            }

            this._vendorSlug = value;
        }

        return this._vendorSlug;
    }.property(),

    ensureVendorLoaded: function (slug) {
        var service = this,
            vendorSlug = slug || service.get('vendorSlug'),
            currentVendor = service.get('vendor'),
            storedVendor;

        if (!Ember.isNone(currentVendor) && (vendorSlug === currentVendor.get('slug'))) {
            return new Ember.RSVP.Promise((resolve) => {
                        resolve(currentVendor);
                    });
        } else {
            var storedVendors = service.store.peekAll('vendor').filter((vendor) => { 
                return vendor.get('slug') === vendorSlug;
            });

            if (storedVendors.length) {
                storedVendor = storedVendors[0];
            }
        }

        if (!Ember.isNone(storedVendor)) {
            service.set('vendorSlug', storedVendor.get('slug'));

            return new Ember.RSVP.Promise((resolve) => {
                    resolve(storedVendor);
                });
        }

        return service.store.queryRecord('vendor', {slug: vendorSlug}).then((vendor) => {
            service.set('vendor', vendor);
            service.set('vendorSlug', vendor.get('slug'));

            return vendor;
        });
    },

    _vendorSlugChanged: function () {
        if (this.get("vendorSlug") === this.get("vendor.slug")) {
            return;
        }

        this.ensureVendorLoaded();
    }.observes('vendorSlug')
});

I would like to be able to assert a couple of scenarios here with the store interaction. Vendor already set, vendor loaded from the peek filter, and vendor loaded from query.

like image 514
BillPull Avatar asked Oct 29 '15 15:10

BillPull


2 Answers

I think I have finally come to a reasonable conclusion. Let me share with you what I think may be the best way to approach unit testing services that rely on the store.

The answer really lies in the assumption we must make when writing unit tests. That is, everything outside of our logical unit should be considered to work properly and our units should be completely independent.

Thus, with a service relying on the store it is best to create a stub or mock (see this question to understand the difference between a mock and a stub) for the store. A stub for the store itself is quite simple. Something like this will do:

 store: {
    find: function() {
       var mockedModel = Ember.Object.create({/*empty*/});
       return mockedModel;
    },
    query: ...
 }

If you prefer to use a mock instead you could do something like the following (i made this really fast so it might not work completely but its enough to get the idea across):

import Ember from 'ember';

class MockStore {
    constructor() {
        this.models = Ember.A([]);
    }

    createRecord(modelName, record) {
        // add a save method to the record
        record.save = () => {
            return new Ember.RSVP.Promise((resolve) => {
                resolve(true);
            });
        };

        if (!this.models[modelName]) {
            this.models[modelName] = Ember.A([]);
        }

        this.models[modelName].pushObject(record);

        return record;
    }

    query(modelName, query) {
        let self = this;

        return new Ember.RSVP.Promise((resolve) => {
            let model = self.models[modelName];
            // find the models that match the query
            let results = model.filter((item) => {
                let result = true;

                for (let prop in query) {
                    if (query.hasOwnProperty(prop)) {
                        if (!item[prop]) {
                            result = false;
                        }
                        else if (query[prop] !== item[prop]) {
                            result = false;
                        }
                    }
                }

                return result;
            });

            resolve(results);
        });
    }
}

export default MockStore;

Next all you have to do is to set the store property (or whatever your calling it) on your service to a new mock store instance when you run a test. I did this like so:

import Ember from 'ember';
import { moduleFor, test } from 'ember-qunit';
import MockStore from '../../helpers/mock-store';

let session;
let store;

const username = '';
const password = '';

moduleFor('service:authentication', 'Unit | Service | authentication', {
    beforeEach() {
        session = Ember.Object.create({});
        store = new MockStore();
    }
});

test('it should authenticate the user', function (assert) {
    // this sets the store property of the service to the mock store
    let authService = this.subject({session: session, store: store});

    authService.authenticate(username, password).then(() => {
        assert.ok(session.get('username'));
    });
});

The documentation on testing these situations is definitely poor, so perhaps there is a better method, but this is what I will be rolling with for now. Also, if you check out the Discourse project, which uses ember, they follow a similar pattern to what I described here, but in a little more advanced manner.

like image 168
cbalos Avatar answered Sep 21 '22 17:09

cbalos


I'm not sure this is the answer you want, but I'll give it a shot anyway. An Ember Service is not really much more than an Ember Object and if you're "unit testing" that Service, it should be in isolation of its dependencies (otherwise it wouldn't be a unit test).

From my understanding (and this could be wrong). If you want to test that service you need to replace the store with a mock implementation.

//tests/unit/services/my-service.js
test('some scenario', function(assert) {
   let service = this.subject({
     store: Ember.Object.create({
        peekAll(modelName){
          //Return array for this scenario
        },
        query(model, params){
          //Return array for this scenario
        }
     });
   });
   assert.ok(service);
});

I also think this is why there's little documentation testing services. One resource I recommend about services is this talk from the Chicago Ember Meetup

like image 44
Pedro Rio Avatar answered Sep 21 '22 17:09

Pedro Rio