Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock an Ember-CLI service in an acceptance test?

Quick summary/tldr:

  • It seems that Ember's container lookup process + Ember-CLI's module resolver doesn't allow manually un-registering a service and then registering a replacement if the original service can be resolved using the resolver (I want to do the method described here, but it doesn't work)
  • How can I mock an Ember-CLI service in an acceptance test without using a hacky, custom resolver? (example project/acceptance test here)

Detailed explanation + example

Create a new service that is injected into a controller:

ember generate service logger

services/logger.js

export default Ember.Object.extend({
  log: function(message){
    console.log(message);
  }
});

initializers/logger-service.js

export function initialize(container, application) {
  application.inject('route', 'loggerService', 'service:logger');
  application.inject('controller', 'loggerService', 'service:logger');
}

The service is accessed through its injected name, loggerService, in an action handler on the application controller:

Use the service in a controller

templates/application.hbs

<button id='do-something-button' {{action 'doSomething'}}>Do Something</button>

controllers/application.hs

export default Ember.Controller.extend({
  actions: {
    doSomething: function(){
      // access the injected service
      this.loggerService.log('log something');
    }
  }
});

Attempt to test that this behavior occurs correctly

I created an acceptance test that checks that the button click triggered the service. The intent is to mock out the service and determine if it was called without actually triggering the service's implementation -- this avoids the side-effects of the real service.

ember generate acceptance-test application

tests/acceptance/application-test.js

import Ember from 'ember';
import startApp from '../helpers/start-app';

var application;
var mockLoggerLogCalled;

module('Acceptance: Application', {
  setup: function() {

    application = startApp();

    mockLoggerLogCalled = 0;
    var mockLogger = Ember.Object.create({
      log: function(m){
        mockLoggerLogCalled = mockLoggerLogCalled + 1;
      }
    });

    application.__container__.unregister('service:logger');
    application.register('service:logger', mockLogger, {instantiate: false});

  },
  teardown: function() {
    Ember.run(application, 'destroy');
  }
});

test('application', function() {
  visit('/');
  click('#do-something-button');
  andThen(function() {
    equal(mockLoggerLogCalled, 1, 'log called once');
  });
});

This is based on the talk Testing Ember Apps: Managing Dependency by mixonic that recommends unregistering the existing service, then re-registering a mocked version:

application.__container__.unregister('service:logger');
application.register('service:logger', mockLogger, {instantiate: false});

Unfortunately, this does not work with Ember-CLI. The culprit is this line in Ember's container:

function resolve(container, normalizedName) {
  // ...
  var resolved = container.resolver(normalizedName) || container.registry[normalizedName];
  // ...
}

which is part of the container's lookup chain. The issue is that the container's resolve method checks the resolver before checking its internal registry. The application.register command registers our mocked service with the container's registry, but when resolve is called the container checks with the resolver before it queries the registry. Ember-CLI uses a custom resolver to match lookups to modules, which means that it will always resolve the original module and not use the newly registered mock service. The workaround for this looks horrible and involves modifying the resolver to never find the original service's module, which allows the container to use the manually registered mock service.

Modify Resolver to avoid resolving to original service

Using a custom resolver in the test allows the service to be successfully mocked. This works by allowing the resolver to perform normal lookups, but when our service's name is looked up the modified resolver acts like it has no module matching that name. This causes the resolve method to find the manually registered mock service in the container.

var MockResolver = Resolver.extend({
  resolveOther: function(parsedName) {

    if (parsedName.fullName === "service:logger") {
      return undefined;
    } else {
      return this._super(parsedName);
    }
  }
});

application = startApp({
  Resolver: MockResolver
});

This seems like it shouldn't be necessary and doesn't match the suggested service mocking from the above slides. Is there a better way to mock this service?

The ember-cli project used in this question be found in this example project on github.

like image 366
CraigTeegarden Avatar asked Feb 10 '15 01:02

CraigTeegarden


2 Answers

Short version of the solution: your registered mock service must have a different service:name than the "real" service you're trying to mock.

Acceptance test:

import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from 'container-doubling/tests/helpers/start-app';

var application;

let speakerMock = Ember.Service.extend({
  speak: function() {
    console.log("Acceptance Mock!");
  }
});

module('Acceptance | acceptance demo', {
  beforeEach: function() {
    application = startApp();

    // the key here is that the registered service:name IS NOT the same as the real service you're trying to mock
    // if you inject it as the same service:name, then the real one will take precedence and be loaded
    application.register('service:mockSpeaker', speakerMock);

    // this should look like your non-test injection, but with the service:name being that of the mock.
    // this will make speakerService use your mock
    application.inject('component', 'speakerService', 'service:mockSpeaker');
  },

  afterEach: function() {
    Ember.run(application, 'destroy');
  }
});

test('visit a route that will trigger usage of the mock service' , function(assert) {
  visit('/');

  andThen(function() {
    assert.equal(currentURL(), '/');
  });
});

Integration test (this is what I was originally working on that caused me issues)

import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import Ember from 'ember';


let speakerMock = Ember.Service.extend({
  speak: function() {
    console.log("Mock one!");
  }
});

moduleForComponent('component-one', 'Integration | Component | component one', {
  integration: true,

  beforeEach: function() {
    // ember 1.13
    this.container.register('service:mockspeaker', speakerMock);
    this.container.injection('component', 'speakerService', 'service:mockspeaker');

    // ember 2.1
    //this.container.registry.register('service:mockspeaker', speakerMock);
    //this.container.registry.injection('component', 'speakerService', 'service:mockspeaker');
  }
});

test('it renders', function(assert) {
  assert.expect(1);

  this.render(hbs`{{component-one}}`);

  assert.ok(true);
});
like image 97
ArtHare Avatar answered Oct 18 '22 14:10

ArtHare


You can register your mock and inject it instead of the original service.

application.register('service:mockLogger', mockLogger, {
  instantiate: false
});

application.inject('route', 'loggerService', 'service:mockLogger');
application.inject('controller', 'loggerService', 'service:mockLogger');

I use this approach for mocking the torii library in my third-party login acceptance tests. I hope there will be a nicer solution in the future.

like image 7
kunerd Avatar answered Oct 18 '22 14:10

kunerd