Testing Ember Component Actions When Components Build On Top Of Components

By Zach Dennis on 17 Mar 2017

Ember components. You know, those itty bitty isolated views of goodness that are often built up on top of other Ember components. It's like Yehuda and Tom watched an episode of Pimp My Ride and got inspired by Xzibit's approach to building a car inside of a car.

The TV blares:

Yo dawg, we put a car inside your car!

Yehuda wide-eyed nearly coughs up his Saturday morning cereal, "Tom, there's something to this!"

Components have evolved into a foundational part of EmberJS. They offer nice boundaries for grouping related areas of behavior while casting the unrelated out.

Surely as Xzibit intended, components can utilize components which in turn can utilize components, so on and so forth. Who needs turtles (or cars for that matter) when we can have components, all the way down.

A common pattern in Ember apps is passing action references from one component, the parent component, in as a property of another component, a nested component sub-component. If we're respecting component boundaries then the nested component is essentially a blackbox. We instantiate it and pass in a set of properties which may include action references. This is so the sub-component can notify upstream objects (e.g. components and controllers) of particular events going on. IF this sounds familiar, it should. It's the Data Down Actions Up (DDAU) mantra that Ember espouses.

When a parent component instantiates a sub-component, it doesn't know what will happen to the properties it passes in. If we pass in an action reference, the sub-component may call it directly, or it may even pass it down to another level of sub-components, which in turn may utilize it directly or pass it down yet another level.

Let's assume that we've both mastered – or at least are comfortable with – creating and using Ember components. Now, let's talk about testing, specifically the component actions.

Questions that come to mind when testing components that use components

  • Should we test the entire component stack individually, all at once, or both?

  • What happens if we have a component that gets used in several other components?

  • Should we test the base component's behavior in all of the other components' tests?

Often, the components that get used inside other components are straightforward and there isn't much benefit to spending a lot of time dwelling over these questions. But, there are times when you either don't want to duplicate the testing of base component behavior.

Why?

The base component may be complex enough that duplicating its set up is cumbersome, makes it harder to understand what we're actually trying to test, and makes it require more effort (and time) to maintain.

This is getting a bit hypothetical, though.

Let's walk through a concrete example

Here's the ember twiddle for the code we're going to walk through. We're going to take a look at what a traditional component test looks like and then see how we can isolate the behavior we're interested in testing from the behavior of sub-components.

Say we have a a todo-list component. This is going to be our parent component and it's going to define the action, clicked, whose behavior we want to test.

Here's the todo-list component JS:

Testing the entire component hierarchy

Here's the todo-list component template:

The above template uses the todo-list-item child component and passes through a reference to the clicked action defined in our todo-list component.

Now, how can we test the clicked action? Keep in mind that in this example we want to observe the state of the DOM (which is why we are using a component integration test). Unfortunately, when using a component integration test we don't get access to the in-memory component itself so we have to interact with the rendered content in some way to exercise the clicked action.

Here's a test that renders the entire component hierarchy and relies on interacting with content/behavior from the todo-list and the todo-list-item components:

For the sake of walking through a straightforward example both components here are dead simple. The test itself is pretty simple.

But what happens if the todo-list-item component were used in five other components? What if the todo-list-item component had more complex logic? Would we want every test that we wrote for a parent component to also test the todo-list-item component each time?

When the answer is yes then the above test works great, but when the answer is no then we may want to separate testing the todo-list-item component behavior from the parent component.

Separating component behavior from sub-component behavior in tests

This is where this next test comes into play. Instead or relying on the real todo-list-item component it takes a different approach by essentially stubbing out the todo-list-item component and template. This allows the test to provide the simplest component to be used so we can show that the clicked action is working when called from a sub-component. After all, what we're interested in is that the clicked action is doing the right thing.

The key to this test is the first few lines that call register. We're basically stubbing out the todo-list-item component JS and template and providing our own simple implementation.

Why?

This lets us ignore how todo-list-item actually works and instead focus on the behavior of the clicked action in our parent component. Our test shows that when we pass through a reference to the clicked action, and when a sub-component calls it, the clicked action did what we expect.

Whether or not this approach is right for your app is something you'll have to decide. It's another tool in the toolbox if you find you need it.

Happy coding!

About Mutually Human

Mutually Human is a custom software design and development consultancy specializing in mobile and web-based products and services. We help our clients design, develop and bring to market innovative products and services based on insightful research and strategy aligned with business objectives. We’ve helped Fortune 500 companies, state governments, and startups.