Injected Dependencies & Spying on Function Calls
Using spies to watch objects in a test
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: Introduction to Test Driven Development PUBLIC
- Lesson 3: Testing Concepts PUBLIC
- Lesson 4: Jest and Cypress PRO
- Lesson 5: A Simple Unit Test PRO
- Lesson 6: A Simple E2E Test PRO
- Lesson 7: Introduction to Angular's TestBed PRO
- Lesson 8: Setting up Tests with Jest and Cypress PUBLIC
- Lesson 9: Test Development Cycle PRO
- Lesson 10: Setting up Cypress & Jest in an Ionic/Angular Project PRO
- Lesson 11: The First Tests PRO
- Lesson 12: Injected Dependencies & Spying on Function Calls PRO
- Lesson 13: Building out Core Functionality PRO
- Lesson 14: Testing Asynchronous Code PRO
- Lesson 15: Creating a Mock Backend PRO
- Lesson 16: Setting up the Server PRO
- Lesson 17: Testing Integration with a Server PRO
- Lesson 18: Testing Storage and Reauthentication PRO
- Lesson 19: Refactoring with Confidence PRO
- Lesson 20: Conclusion PRO
Lesson Outline
Testing Injected Dependencies & Spying on Function Calls
In the last lesson we successfully implemented the first requirement for our application, now we are going to move onto the second requirement that we identified:
- After selecting a specific module, the user should be able to see a list of available lessons
Assuming that in order to view a list of lessons for a specific module we would send the user to another page, this requirement is clearly going to rely on testing navigation. In the context of an E2E test this is not really an issue, an E2E test tests our application as a whole which includes navigating from page to page.
When testing navigation in the context of a unit test there are a few issues that pop up. We have been very insistent on the fact that a unit test should be isolated. In our unit tests, we use Angular's TestBed to create an isolated testing environment where that one component is the only thing that exists, and we would even mock the NavController
using a fake object that does absolutely nothing.
On the surface, this makes it seem like it's pretty impossible to test navigation. But it's not an issue, we don't need to actually navigate to the page in the unit test, we just need to check that the appropriate call is made. This is where spies become extremely useful. We will take a look at the concept of mocks/spies in-depth in just a moment once we circle back around to creating some units tests, but first, let's continue on with the E2E tests.
Testing Lessons
We've got a lot of tests to create in this module, so we're going to pick the pace up a little bit this time.
Let's take another look at the requirement that we are trying to implement:
- After selecting a specific module, the user should be able to see a list of available lessons
We will start implementing this functionality in the same way we implemented the functionality in the last lesson: with an E2E test. This test is going to involve us interacting with two separate pages, we will need to:
- Click on a module on the home page
- Check that a list of lessons is available on the LessonSelectPage
In order to click on a module on the home page we will need to grab a reference to it, and in order to check that lessons are available on the Lesson Select
page we will need to grab a reference to that list. Grabbing the module on the home page is fine because we already have a helper function to do that, but we don't have a function to grab the lesson list from the (as of yet, non-existent) LessonSelectPage
.
We are going to set up another helper function to grab the lesson list items, and we are also going to set up another custom command to automatically navigate to the LessonSelectPage
.
Add the following helper function to cypress/support/utils.ts:
export const getLessonListItems = () =>
cy.get('[data-test="lesson-list-item"]');
Modify cypress/support/commands.ts to reflect the following:
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable<Subject> {
navigateToHomePage(): typeof navigateToHomePage;
navigateToLessonSelectPage(): typeof navigateToLessonSelectPage;
}
}
}
const navigateToHomePage = () => {
cy.visit('/');
};
const navigateToLessonSelectPage = () => {
cy.visit('/');
};
Cypress.Commands.add('navigateToHomePage', navigateToHomePage);
Cypress.Commands.add('navigateToLessonSelectPage', navigateToLessonSelectPage);
export {};
At the moment, our navigateToLessonSelectPage
command does exactly the same thing as the home page command, but we will be updating that later. Remember, each time we want to add a new command we will need to:
- Create a function to execute the logic we want
- Add the function as a command using
Cypress.Commands.add
- Define the new function inside of the
Cypress
namespace
In future, we will just cover the creation of the function. You will need to remember to add the command appropriately and extend the Cypress interface yourself.
Now let's utilise these in a new test that will test navigating from the home page to the lesson select page.
Modify cypress/e2e/home.cy.ts to reflect the following:
import { getLessonListItems, getModuleListItems } from '../support/utils';
describe('Home', () => {
beforeEach(() => {
cy.navigateToHomePage();
});
it('should be able to view a list of modules', () => {
getModuleListItems().should('have.length', 5);
});
it('the list of modules should contain the titles of the modules', () => {
getModuleListItems().first().should('contain.text', 'Module One');
});
it('after selecting a specific module, the user should be able to see a list of available lessons', () => {
getModuleListItems().first().click();
getLessonListItems().should('have.length.greaterThan', 0);
});
});
Now we are also importing our new getLessonListItems
helper function from our utils file. In our new test, we first get the first module in the list on the home page and then click it using the click
method. Then we grab the lesson list items and we check that the number of items in that list is greater than 0
.
Run the E2E tests
You should see the following error:
get [data-test="lesson-list-item"]
assert expected { Object (length, prevObject, ...) } to have a length above 0 but got 0
In our test we are expecting that the length of getLessonListItems()
should be greater than 0
, but this test is failing because the resulting length is 0
... which is not greater than 0
.
We are expecting this test to fail because that's what we do when we create our tests - we write them, and then we watch them fail first before doing anything else. You might intuitively know specifically why it is failing and what we should do about it, but if you're not sure where to begin, you might want to make use of:
- The super magic time travelling abilities that Cypress provides
Cypress is pretty amazing in helping to understand why a particular test is failing. You might notice that it lists what it is doing step by step:
We can see that this test has taken the following steps:
- Visit the
/
route (this is executed within thebeforeEach
) - Get elements that match the selector
[data-test="module-list-item"]
- Get the first element from that result
- Click it
- Get the elements that match the selector
[data-test="lesson-list-item"]
(0
elements found) - Expect that more than
0
elements were found (fails)
This is pretty descriptive and helpful already, but what you might not immediately notice is that you can hover over each step to see a snapshot of exactly what was going on in the DOM at that moment in time. Just hover your mouse over each of the steps and you will see the DOM snapshot changing, we can see snapshots of:
- The
/
route loading - A reference to all the modules being created
- A reference to the first module being created
- What the DOM looks like before the
click
- What the DOM looks like after the
click
This makes it clear that the problem is that clicking on one of the modules doesn't actually do anything. Clicking the module doesn't currently do anything, so the page will remain the same. Without having moved to the next page, the test now looks for any elements that match the [data-test="lesson-list-item"]
selector, which is 0
.
Now it's time to start working on our unit tests so we can get this test passing. A good place to start would be to make the module actually do something when it is clicked.
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).