Lesson 12

Injected Dependencies & Spying on Function Calls

Using spies to watch objects in a test

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:

Time travelling in Cypress

We can see that this test has taken the following steps:

  1. Visit the / route (this is executed within the beforeEach)
  2. Get elements that match the selector [data-test="module-list-item"]
  3. Get the first element from that result
  4. Click it
  5. Get the elements that match the selector [data-test="lesson-list-item"] (0 elements found)
  6. 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.

PRO

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).