I've written a blog post talking about the different levels of checking in an Ember.js application. Link to it here
An Ember.js application looks like this:
If we have a function as:
fullName: computed('fullName', function() {
const firstName = this.get('firstName');
const lastName = this.get('lastName');
if(lastName) {
return `${lastName}, ${firstName}`;
} else {
return firstName;
}
}
And the single computed attribute can be checked using at the unit level:
test("should display last name and first name in order", function(assert) {
// get the controller instance
const controller = this.owner.lookup("controller:names");
controller.set("firstName", "Michael");
controller.set("lastName", "Jordan");
assert.equal(someThing.get("fullName"), "Jordan, Michael");
});
test("should display only the first name if no last name exist", function(assert) {
// get the controller instance
const controller = this.owner.lookup("controller:names");
controller.set("firstName", "Michael");
controller.set("lastName", null);
assert.equal(someThing.get("fullName"), "Michael");
});
Now, if you want to investigate how this attribute is rendered (controller + template), you would go with a Component check:
test('displays full name with first and last name', async function(assert) {
this.set('firstName', 'Mary');
this.set('lastName', 'Johnson');
await render(hbs`
{ {fullNameDisplayer} }
`);
assert.equal(this.element.querySelector('.full_name').textContent, 'Johnson, Mary');
}
In this case, we are isolating completely the rendering of the component from the application as a whole.
To ensure that this component is well integrated with the routing, you can create an (Ember) Application level check:
test("should say welcome at landing page", async function(assert) {
await visit("/");
assert.equal(
this.element.querySelector(".welcome_message").textContent,
"Welcome!",
"should say 'Welcome'"
);
});
This check can run a whole Ember application, isolating the services using Mirage.
These services should be tested in isolation, surely. But how to confirm that services provide the same structure of response as that ones you've mocked using Mirage?
That's where the concept of Contract Testing enters, using a tool such as Pact, which replays the requests issued by a consumer and checks if the responses match the consumers mocked data.
Now that we've checked that everything works as expected and can be connected, the last piece of the puzzle is checking if that are actually connected. Let's say your frontend app is configured to search for a service where it doesn't exist. Everything will pass, include Contract Testing, but your app will not work. This an example where end-to-end (Selenium, e.g.) comes handy, because it uses the app as a user would do, considering the tool's limitation, of course.
I talk about this and other examples of the blog post.
Each of these checks provide one type of information and are essential to have a good response time to a problem. If you have a problem that could be detected by an unit test, but you just have end-to-end checks, your response time (debugging + fix) will be longer; however, as shown, not everything can be detected by lower levels of checking.
If you use each tool with its best goal in mind, the check you write would be clearer and more objective.