How to use a service from an acceptance test in Ember.js

A quick google for this question brings up a ton of old information that doesn’t work. Here’s an example of how to get it done in Ember 2.x and a peek into the future for an approach that doesn’t rely on private API.

Thanks to ember-cli/testing extraordinaires Tobias Bieniek (aka Turbo87) and Scott Newcomer for reviewing to make sure I wasn’t too far off the rails, plus sharing the path forward for testing services in Ember!

The dangers of using _underscored and private methods

First, a disclaimer. When using many JavaScript libraries, you often have access to more methods, properties, and functionality than you are supposed to actually use.

The public API represents what is safe, the methods and properties meant for you. For projects that follow semver, the behavior of those methods won’t be changed in such a way that breaks your app, unless you’re upgrading by major version. Plus, Ember.js does a great job making it as easy as possible to resolve those breaking changes through deprecation warnings, which are available far in advance of the actual change. If you stick to public API, you’ll have a much better time. Unfortunately, if all API was public, the people who work on the library would never be able to change how it works internally, and all progress would grind to a halt.

On the other hand, the private API is always subject to change, possibly without warning, and possibly without a migration strategy. So, what can happen if you rely on private API is, one day your app is fine, then you try to upgrade versions, and it stops working. Without an npm package lock, this could happen without you even intentionally upgrading, since npm dependencies (and the dependencies of your dependencies) use punctuation like “^” and “~” to flexibly grab more recent versions. Yarn (an npm alternative) doesn’t try to freeze the versions of your dependency’s dependencies. Imagine that you’re ready to ship an app, and suddenly it doesn’t work anymore, or you’re stuck on an older version indefinitely. Those are just a couple examples of the bad things that could happen when you use private API.

So how do you tell the difference between private and public API?

It’s all in the documentation. By default, you’ll only see public api when you visit https://emberjs.com/api. If you followed a link from somewhere else, you might see some private API, but it will be labelled. Within the source code for Ember.js, there are code comments that have a @public label on the methods and attributes that you are “allowed” to use.

Look out for things that use _underscores in the method/property. That’s a huge red flag that something is private!

Also, don’t assume that whatever you find on Stack Overflow/blog articles like this is safe to use. Check the docs before you copy and paste.

But what if I need to use private API?

First, can you accept the risk that it might change without warning? In my case, I needed to write a test, and I only needed to use private API for one test. If my test breaks, so what? I remove the test and find another way. My CI might break, but my app isn’t broken.

Second, have you communicated the need for a public method? Ember.js uses the RFC (Request for Comments) process to evaluate new features or deprecations. Anyone in the community can start one or open an issue to discuss before creating an RFC. For example, this RFC proposes an easier way to look up services! It’s part of a larger plan to make testing even more powerful and intuitive. Later in this article, I’m sharing a technique that’s currently private API, but it’s on its way to being public thanks to the RFC process. When an RFC is accepted/merged, that doesn’t mean that the work to implement the feature is done, but it means it’s the plan.

Lastly, are you sure that you need the private API? Like, really sure? The Ember Community Discord and the API Docs are good places to do some research.

Alternative #1: stub the service instead

Instead of trying to use a real service (and using private API), you might be able to stub the service instead! Check out this Acceptance Test example adapted from the official Super Rentals tutorial:

import { test } from 'qunit';
import moduleForAcceptance from 'super-rentals/tests/helpers/module-for-acceptance';
import Service from '@ember/service';

let StubMapsService = Service.extend({
getMapElement() {
return document.createElement('div');
}
});

moduleForAcceptance('Acceptance | list rentals', {
beforeEach() {
this.application.register('service:maps', StubMapsService);
}
});

Alternative #2: unit test your service instead

It’s important to ask yourself, what are you testing and why? With acceptance tests, it’s easy to try and test too much at once. They are really meant for checking user interactions, not testing internal behavior. It’s possible that what you should really be doing is writing a unit test for your service plus an integration test for your component.

Still need to really actually use the service in your acceptance test?

I have good news. If you can upgrade to the latest version of ember-cli-qunit, using a service is as simple as:

this.owner.lookup('service:servicename')

import { module, test } from ‘qunit’;
import { setupApplicationTest } from ‘ember-qunit’;
import { visit } from ‘@ember/test-helpers’;
module(‘Acceptance | current url’, function(hooks) {
setupApplicationTest(hooks);

test(‘visiting /current-url’, async function(assert) {
await visit(‘/current’);
let page = this.owner.lookup(‘service:page’);
let currentVersion = page.get(‘currentVersion’);
assert.equal(find(‘.ember-basic-dropdown-trigger’).text().trim(), currentVersion);
});
});

thanks to amazing work being done following the testing RFCs. You can read more in this article by Robert Jackson.

If you are stuck on an older version of ember-cli-qunit, here’s an approach that uses private API to get the job done. You’ll probably have to come back and update it later down the road — such is the price of using private API.

// WARNING private API subject to change
let myService = this.application.__container__.lookup(‘service:myServiceName’)

In the context of a real test:

import { test } from ‘qunit’;
import moduleForAcceptance from ‘guides-app/tests/helpers/module-for-acceptance’;
moduleForAcceptance(‘Acceptance | current url’); 
test(‘visiting /current-url’, function(assert) {  
visit(‘/current’);
// WARNING the next line relies on private API, subject to change
let page = this.application.__container__.lookup(‘service:page’);
andThen(function() {
let currentVersion = page.get(‘currentVersion’);
assert.equal(find(‘.ember-basic-dropdown-trigger’).text().trim(), currentVersion);
});
});

You didn’t get this from me ;)