Unit-testing Glimmer Components

7 November 2019

Update: Some amazing sleuthing by Discord user DanDan has not only uncovered breaking API changes, but also cleaned up some of the component manager lookup code. Thanks so much, DanDan!

Like many Ember developers, I’m excited about Glimmer. With the Octane release, the Ember team has removed a lot of cruft from the framework and dramatically simplified day-to-day development. As such, I was excited to upgrade to the latest releases that support Glimmer—3.13 and 3.14—and try out the new goodies.

Unfortunately, I quickly ran into what seemed like a strange miss on Glimmer’s part: Glimmer components don’t appear to support unit tests. Most of my Ember projects use a large number of component unit tests, so this was a bummer to discover: I’d have to make major changes to the way my components worked in order to have test coverage.

The Problem

A default component unit test (created via ember generate) looks something like this:

test('it exists', function(assert) {
let component = this.owner.factoryFor('component:my-component').create();

This test is pretty easy to understand: the test looks up the factory function for the component we specify, then creates an instance, returning a reference so we can make assertions against the component, minus its rendering. I use this kind of test all the time to exercise computed properties, so I naturally started here when looking to test Glimmer components.

Running said test, however, provided the following error:

Error: Failed to create an instance of ‘component:my-component’. Most likely an improperly defined class or an invalid module export.

I asked around on Ember’s Discord (a great place to get help, if you haven’t discovered this already), but there was no obvious solution. I tried a couple of other suggested options, all to no avail:

new (this.owner.factoryFor('component:my-component').class);
new MyComponent(this.owner.ownerInjection(), {});

Error: You must pass both the owner and args to super() in your component.

What I was provided with, though, was some background on Glimmer’s internals and some discussion on component testing in general: just enough to get the little grey cells moving.

To the source!

Fortunately, Glimmer’s source (and Ember’s, too, really) is very readable, and it didn’t take long to grok how Glimmer’s components work: arguments passed to components are given to a ComponentManager, which then passes them to newly-created Components after confirming that the args’ integrity is intact. Can we then use this same approach to create components for unit testing? In short: yes!

The Owner. First, we’ll need a reference to the “owner”: a shortcut into Ember’s dependency injection container. If we’re already inside a test, we can access this via this.owner; otherwise, we can use a function from @ember/test-helpers:

import { getContext } from '@ember/test-helpers';
let { owner } = getContext();

The Factory. We also need a factory function to give to the Component Manager so it can create the Component instance. Historically, this was in a factoryFor() function, and it still is: just behind a class property (since we’re using classes now!). To get this, we’ll need the lookup key for the component (usually of the style of component:my-component). If you’re migrating an existing unit test, this is the string passed to factoryFor():

let lookupPath = 'component:my-component';
let { class: componentClass } = owner.factoryFor(lookupPath);

The Component Manager. Next, we need to create the aforementioned Component Manager, through which we’ll pass arguments to our soon-to-be-created component. The component manager is injected into Ember’s dependency container, so we can import it thusly:

let componentManager = owner.lookup('component-manager:glimmer');

At last: the Component! Now that we have an Owner and Component Manager, we can create the component we’ve always wanted! We’ll need one last thing: any arguments we want to provide the Component. The Component Manager expects these in an attribute named named, so we’ll oblige it:

let named = { myArgument: 42 };
let component = componentManager.createComponent(componentClass, { named });

The Tests. The component alone is not enough, of course: we still need our tests. I was concerned about this part, because Glimmer components do away with computed properties and Ember’s getters and setters. I needn’t have worried, though: Octane’s changes mean most Glimmer component code is regular JavaScript! Just set values as you would inside the Glimmer component, and you’re all good!

named.myArgument = 1337;
assert.equal(component.someGetterThatUsesMyArgument, 1337);

Don’t Repeat Yourself

I use this approach often enough that I usually create a test helper to do all the hard work for us in one place. Here’s the helper in its entirety:

import { getContext } from '@ember/test-helpers';

export default function createComponent(lookupPath, named = {}) {
let { owner } = getContext();
let componentManager = owner.lookup('component-manager:glimmer');
let { class: componentClass } = owner.factoryFor(lookupPath);
return componentManager.createComponent(componentClass, { named });

And you can use it thusly:

import createComponent from 'my-app/tests/helpers/create-component';

test('should calculate the value', async function(assert) {
let model = { val: 4 };
let component = createComponent('component:my-component', { model });
assert.equal(component.valueSquared, 16);

model.val = 3;
assert.equal(component.valueSquared, 9);

I’ve published a sample Ember Octane app on GitHub if you’d like to play around with real code.

A Disclaimer

As I mentioned earlier in this post, Octane doesn’t officially support unit tests for Glimmer components. Although (as far as I can tell), everything in this post uses non-private APIs, there’s no guarantee this approach will work in the future. Of course, I hope the Ember and Glimmer teams restore official support for this type of test, as it’s usually the first thing I reach for when building out components. Until then, I’ll try to keep up with any API changes and update this post to match.

I want to extend special thanks to NullVoxPopuli, pzuraq, and bendemboski from the Discord server, for listening to my concerns, patiently explaining me through the Glimmer rendering process, and offering suggestions on how to proceed.

I also owe Discord user DanDan a debt of gratitude for letting me know of the breaking Glimmer API and his work to resolve it. Much appreciated!

Do you have any other approaches you use for component tests? Suggestions for improvement? Let me know in the comments below, and happy testing!