Ember Component Tests: The Pragmatic Middle
As an application grows in scope and complexity it can be easy to end up with pages that have
many operations they're responsible for. What started as a set of CRUD screens grows to include
search, navigation, authentication, internationalization, websockets, you name it. Complicated pages can also make for complicated tests! Components are a great way to break those big pages into smaller chunks that you can reason about separately, and leverage to improve your test suite.
A Quick Note on Terminology
When I talk about types of tests in Ember I'm going to generally defer to the terminology that's used in the guides.
-
An acceptance test will run your whole app. It's a test that visits a url, renders a page, and interacts with the DOM. If you're using a newer ember-cli build you'll be using
moduleForAcceptance()
to run those tests, otherwise you'll rely on the good oldmodule()
. -
A unit test pulls out a specific module (a component, a controller, etc) from your app and tests it in isolation. You tell the test harness exactly what you need. In
ember-qunit
you'd usemoduleFor()
to set up a unit test.
Acceptance Test Bookkeeping
I mentioned before that complicated pages make for complicated acceptance tests. Since an acceptance test treats the app as a black box, interacting with a complicated page is still easy. Getting the page to render in the state you want it in ends up being the issue. We'll start with a simple example that we'd want to test and go from there.
Imagine a page that shows the first and last name for a given User:
The route could be something as simple as this:
Before we write any actual test code, it's helpful to describe the whole thing in plain English. If the test doesn't sound worth running before you code it up, don't write it in the first place!
Given a User, when I visit that User's page, then I should see the User's first and last names.
If the precondition is an existing user, how should we go about making sure that condition is satisfied for the test? From the implementer's point of view, the test statement becomes something more like:
Given a certain User can be found in the store, when I visit ...
Displaying a single entity is easy, but now imagine that our page is a little more complicated. Let's say users have lists of Vacations, and if they're authorized they can see them and create new ones:
The users/vacation-list
component is made up of a few other components wired together:
We'll at least want to have a smoke test that makes sure the vacation-list
component is working. The English version of our preconditions would be closer to:
Given a User and their Vacations can be found in the store, and given the user is authorized to see them...
Now whatever we're doing in our test setup will have to create some valid Vacations for the User. We'll also have to make sure we deal with the Authorization service.
As the page does more, every developer who writes acceptance tests for that page needs to
be aware of the whole application lifecycle up to the point where the thing they care about for their particular test case is rendered. You can mitigate the time it takes to create preconditions with a good suite of helper functions1, but it still takes its toll. With a big page it also becomes likely that detailed test cases will only end up touching a few parts of that page, even though we're spending the time to render the whole thing.
Fortunately for us Ember's test helpers will allow us to test the rendered behavior of the vacation-list
component separately! We don't have to keep running the whole application just to test a little slice of it.
Component Integration Tests
'Component Integration tests' are Ember parlance for what most people could consider a unit test in terms of scope. We'll be able to test the rendered state of a component (or nested group of components) in isolation from the rest of the app. The following file is close to the default one generated by ember-cli
's blueprint:
Without integration: true
, moduleForComponent
functions a lot like moduleFor()
. In integration mode the test context (this
inside the test function) has access to some special functions you'll use to set up the integration test.
-
this.render()
: The preferred argument to pass torender()
is a precompiled template. Newer test boilerplate will include
the 'htmlbars-inline-precompile' package and you can use it like I've done above. The template you render doesn't even have to correspond to the argument you gave tomoduleForComponent
if you so desire. If our component only worked properly by taking a block we could wire up arbitrary children inside of it and make sure everything was working correctly. -
this.set()
: An important thing to note is that the rendering context for the template is the test context. Usethis.set()
orthis.setProperties()
to add variables to the test context, like I did withuserToTest
in the previous snippet.
The bindings to the test context act just like any other rendering context in Ember. If you set()
a value and then mutate it in the component, this.get()
will return the new value.
-
this.on()
: Like the blueprint's helpful comment suggests,on()
will connect an action handler to the test context. Sinceusers/vacation-list
uses a closure action forcreate
, the argument can't be null. For more info on closure actions the original rfc is still probably the best source.
I'll cover actions in a more meaningful way in a bit. -
this.container
: We've got access to the full container for these integration tests! Unlike unit tests (moduleFor
), integration tests don't take a
needs:
option to specify their dependencies. In the above example we're looking up
the store to create a record for our test. If your component uses Services for any of its functionality you can easily swap out those dependencies before rendering. -
this.$()
:$()
with no arguments will select the outer html of the rendered element. You can also call it with any jQuery selector.
An important difference between component integration tests and a full acceptance test: initializers do not run during component integration test setup. If you're relying on them you'll need to individually import and run each one yourself before rendering.
Interacting with Rendered Components
We've seen how we can render a component to the screen and assert against the results. Now let's take a look at how we can interact with it.
In component tests we don't have access to the async test helpers (click
, fillIn
, etc) nor are we expected to put our assertions into an andThen
. All our interactions will be synchronous, just like rendering. At the component level it's more straightforward to directly trigger the appropriate DOM events on the element itself after grabbing it with this.$()
. Here I'm setting a value in a text field and firing change
:
Testing Actions
Entering text is great and all, but we still need to assert that the actions coming out of the component are behaving properly. Now we can have the stubbed action that we've been passing vacation-list
assert against its arguments when it's called:
Asynchronous Operations
What if the component uses the run loop or does asynchronous work internally? We're in luck! The ember-test-helpers package that we're already using has a wait helper. wait()
returns a promise that resolves when all pending run loops and ajax requests have completed.
Component Tests Inform Component Design
Thinking about a page or a component as a function helps bring me some perspective on how to test it well. Imagine I came to you and said, "Here's a 300 line function that takes 12 arguments and also uses a few global variables. Let's test it!" You'd be remiss in leaving that thing as-is. You'd break it down into smaller pieces, test those separately, and have a few tests to make sure the smaller functions all worked together properly. That's exactly the progression we can take when refactoring a big page into components.
Start from the Middle
A good test suite is going to have tests with a variety of scopes. It's essential to have end-to-end tests that confirm your app's pieces are correctly wired together, and for complex logic it's probably going to make sense to use isolated unit tests. A large chunk
of your app, however, will probably fall into a middle ground where the most sensible thing to do will be to test groups of components
as singular units. You'll be able to reason about and test those chunks separately without having to over-test the pieces that happen
to get rendered on the same page. The middle ground is a pretty happy (well-tested) place.
Footnote: Stubbing AJAX Requests
For our acceptance tests I've generally stubbed the ajax response using sinonjs. I've also had good luck using
ember-data-factory-guy as our payloads have gotten more
complicated, especially in the case of endpoints that do lots of sideloading. I've been hearing good things about ember-cli-mirage, but haven't had time to investigate firsthand yet.