Lesson 5

[Sprint One] Testing Authentication

Using our test strategy for admin authentication

PRO

Lesson Outline

Testing Authentication

At the end of the last lesson I hinted that once we get past a lot of this set up sort of stuff our TDD journey will become a lot smoother. This lesson is not quite that though - but on the bright side, this lesson is the last that involves difficult setup/configuration kind of work. This is the first lesson where we will actually start writing some E2E tests with Cypress.

Whilst using Cypress is quite developer friendly, getting Firebase Authentication and Cypress to play nicely isn't so friendly. The fundamental problem we will face in this lesson is that we want to write E2E tests to test certain behaviours in our application, but our entire application is going to be behind an authentication wall. Although this isn't the ideal solution, it is possible in a lot of situations to just have your E2E test go through the authentication process just as a normal user would (this isn't ideal because this would slow the tests down a great deal). However, we can't even do that because the Firebase Authentication flow includes a pop up window that Cypress won't have access to! Don't worry though, we have a solution for this - it's a bit of work but we will cross that bridge when we get there.

Let's quickly recap what we are actually working on here.

Feature: Log in to the application

User story: As a massage therapist, I want to be able to log in to the application using my Google account from any device, so that only I can access private information in the application and I can do it from any device

We're done with our security rules for now, and now we are going to do some more standard application testing with Cypress/Jest. Remember, if you don't already have at least a basic understanding of Cypress/Jest then I would recommend completing Test Driven Development with Cypress/Jest first.

We've already mentioned the general TDD process we will be using, but let's recap it again:

  1. Write a failing E2E test with Cypress that covers the user story you are implementing
  2. Write a failing unit test to address the error the E2E test raises
  3. Implement code to make the failing unit test succeed
  4. Check if the E2E test now passes
  5. If the E2E test still does not pass, write more unit tests until it does

Keep in mind that for the above process I will generally use the term "unit test" to mean either an actual unit test or an integration test - sometimes we might use one or the other or both depending on the circumstance. The key point is that we are using some kind of lower level testing strategy to help satisfy the E2E test.

This is a very brief description/overview, so again, I'd highly recommend you go through that other module if you are not familiar with this process as I won't be discussing how to test in great detail throughout this module. We will mostly just focus on anything that is particularly interesting, and all of the basic testing stuff I will skip over.

Creating an E2E Test

Ok, let's get started with our first E2E test.

Rename the default cypress/e2e/spec.cy.ts file to home.cy.ts

Now we want to create a test that captures the essence of our user story:

As a massage therapist, I want to be able to log in to the application using my Google account from any device, so that only I can access private information in the application and I can do it from any device

describe('Home', () => {
  it('can log in and view dashboard', () => {
    cy.visit('/');
    cy.get('[data-test=login-button]').click();
    cy.get('[data-test=page-title]').should('contain.text', 'Clients');
  });
});

NOTE: We can use any CSS selector we want for cy.get but to conform with best practices we will use data-* attributes to select elements for our test. This will allow us to attach an attribute like data-test='login button' to an element, and then select that in the test. This makes our tests less brittle, because if we rely on selectors like classes (e.g. .login-button) we might inadvertently break our tests when refactoring our code.

This test will go to the home page, click the login button, and then wait to see if we end up on the admin dashboard. Remember that none of these elements we are clicking or the things we are checking for even exist yet - we just write the tests in the way we want it to work.

To keep things clean and allow for reuse, we are going to separate our our get calls into utility files:

Create a file at cypress/support/utils.ts and add the following:

export const getLoginButton = () => cy.get('[data-test=login-button]');
export const getTitle = () => cy.get('[data-test=page-title]');

Refactor the test:

import { getLoginButton, getTitle } from '../support/utils';

describe('Home', () => {
  it('can log in and view dashboard', () => {
    cy.visit('/');
    getLoginButton().click();
    getTitle().should('contain.text', 'Clients');
  });
});

For the sake of keeping things official, let's run this test and make sure it fails. I'm sure that it will, but it is important that we actually see the test fail at some point - if your test succeeds before you even do anything then it won't be protecting you (worse, it will lead to a false sense of security).

npm run e2e

This leads to the following error:

Timed out retrying after 4000ms: Expected to find element: [data-test=login-button], but never found it.

Great, we have a failing test.

Now, finally, we can actually start working on our login implementation! For this first bit of functionality I will go through the testing process step-by-step in detail, but since this module isn't specifically about learning how to use TDD, most of the rest of this module won't talk through the minor details.

We should use our failed E2E test to help drive out the unit tests that we need to build to get it to pass. Let's take a look at that error again:

Timed out retrying after 4000ms: Expected to find element: [data-test=login-button], but never found it.

The test is failing because it was looking for a login button to click, but it never found it. In response to this, we should add a login button. To recap, this is generally how I go about creating tests:

  1. Create an E2E test
  2. Use the E2E test to define a unit test that will help solve it
  3. Implement functionality to solve the unit test

Although you can test the DOM with a unit test, I generally prefer to leave this up to my E2E tests wherever possible. So, in situations like this where we just need to add something to the template to get the test to pass, I won't bother to create a separate unit test. My justification for this is that the E2E test is already specifically testing that, and adding a unit test to do the same thing doesn't really achieve anything for me.

What you should be careful not to do, however, is implement more than what the failure is indicating. It wants a button to click, so I am going to give it a button to click. You might be tempted to also add in what the button is supposed to do (e.g. trigger the login process with Firebase), but this would be getting ahead of ourselves as we don't have a failing test for that.

Let's add in that login button:

Modify src/app/home/home.page.html to reflect the following:

<ion-header>
  <ion-toolbar>
    <ion-title> Refresh </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button data-test="login-button">Login</ion-button>
</ion-content>
PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).