Imagine you have the following component:
where grades looks like:
["A", "A", "C", "A", "B", "D", "B", "B"];and the template looks like:
The student-grades-donut component is a light wrapper around the donut-chart component to handle the specifics of manipulating a student's grades into the format donut-chart expects. That is, gradeData is computed from grades into a format like:
{
columns: [
["A", 3],
["B", 3],
["C", 1],
["D", 1],
["F", 0],
];
}This is pretty straightforward to implement, but how do you test the student-grades-donut component?
A Few Approaches
-
One approach is to write assertions against the generated SVG from
donut-cart. I don't like this approach for a couple reasons. First, writing assertions against the SVG doesn't really verify that thedonut-chartrendered correctly. A visual check would be more helpful. Second, if the resulting DOM fromdonut-chartchanges, then our test could break, especially ifdonut-chartis a third-party component or a wrapper around a library like C3.js or D3.js. -
Another approach is to write a unit test for the
student-grades-donutcomponent verifying that the computed propertygradeDatais in the correct format. Component tests are integration tests by default in Ember CLI, but you can also generate a unit test for a component with:ember g component-test student-grades-donut --unit. I also don't like this approach because thegradeDatacomputed property would be exposed in the test and treated like a public property. When I think of a component's public API, I think of the attributes passed to the component likegradesor anything that the componentyields, andgradeDatais neither one of those. If the implementation of how this grade data is formatted changes and the property name changes, the test will also have to update. Not a big deal, but still not ideal. -
A third option is not to test this component and chalk it up to one of those areas in an app that isn't tested. 😛
Solution
What would make me feel confident that this feature works correctly is to test that the grade data is formatted correctly and passed into the donut-chart component, and ideally not have to reference the gradeData computed property explicitly in our test since it is more like a private property. This approach could treat donut-chart like a spy and record the arguments (component attributes) that it was called with. It turns out we can easily do this without a test double library like Sinon.js, thanks to Ember's container.
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import Ember from 'ember';
moduleForComponent(
'student-grades-donut',
'Integration | Component | student grades donut',
{
integration: true,
}
);
test('the donut-chart is invoked with the grade data properly formatted', function (assert) {
this.register(
'component:donut-chart',
Ember.Component.extend({
didReceiveAttrs() {
assert.deepEqual(this.get('data'), {
columns: [
['A', 3],
['B', 3],
['C', 1],
['D', 1],
['F', 0],
],
});
},
})
);
this.set('grades', ['A', 'A', 'C', 'A', 'B', 'D', 'B', 'B']);
this.render(hbs`{{student-grades-donut grades=grades}}`);
});Components are resolved out of Ember's container. In integration tests, we can register things with the container via this.register(). You may have used this before to stub out a service that gets injected into a component. Instead of registering a stub service, we can register a stub component. When the student-grades-donut component is rendered, the donut-chart component will get invoked from the template, and our stubbed Ember.Component class for donut-chart will get resolved out of the container and instantiated. The didReceiveAttrs() hook will get called, at which point we can assert against the data attribute for donut-chart. With this approach, we can check the value of gradeData and verify that it was passed into donut-chart under the data attribute without ever having to explicitly reference the gradeData computed property! Our test now only relies on public API.
Conclusion
In integration tests, not only can we stub services, but we can also stub components. I've found this technique useful when a component processes some data and passes that data along to a more generic/reusable component that might be harder to test, like a chart or a map.

