Testing web applications with Cypress
Testing is a crucial quality control aspect in software development. It involves evaluating a target, i.e., a service or a component, to find out if it meets the specified requirements of the software or the developed service. In other words, it's like executing a system to identify any gaps, errors, or missing requirements in contrast to the actual requirements.
Testing = Checking (does the software behave as intended under conditions it's supposed to handle?) + Exploring (are there any other risks?) - From the book Explore It!: Reduce Risk and Increase Confidence with Exploratory Testing, by Elizabeth Hendrickson
Table of Content:
What I am going to tell in this blog ?
Previously, I developed a web application for city bike users. At that point in time, I did not test the application. I noticed afterward that when I update the code base to include new features and update the software dependencies, some of the earlier developed features break unknowingly. So, I have decided to write test cases to check that they behave accordingly.
In this blog post, I will explain Component and End-to-End testing for the login feature. Remember that, in my city bike application, a user logs in with their Google accounts, and the account session is preserved until the user logs out.
Cypress supports both Component and End-to-End testing for web applications. Component testing involves testing each part or component of a software application separately, with or without isolation from other objects. Whereas, End-to-End testing focuses on verifying the complete flow of a software application from start to finish.
Set up Cypress for End-to-End and Component testing
I followed these simple steps to get started:
Writing End-to-End test cases with Cypress
The goal of end-to-end testing is to provide confidence that the software works as intended in a real-world context and to uncover any defects or issues that may arise when different parts of the system interact. I will use the following scenario to explain this.
Scenario: As a user of the City bike application, I can log in with a Google account and the logged-in user's profile information is shown correctly.
I achieve this goal with the following steps:
Gather application credentials and a refresh token for a test user
To write test cases for Google login, the first step is setting the credentials for the application and the test user. For this:
Configure the application and the test user's credentials at the Cypress environment
First, create a .env file to store these credentials
Next, I import these to the cypress environment with the following code at cypress.config.ts
import { defineConfig } from "cypress";
// Populate process.env with values from .env file
require("dotenv").config();
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3001",
},
env: {
googleRefreshToken: process.env.GOOGLE_REFRESH_TOKEN,
googleClientId: process.env.REACT_APP_GOOGLE_CLIENTID,
googleClientSecret: process.env.REACT_APP_GOOGLE_CLIENT_SECRET,
},
});
The export default defineConfig defines three variables, assigns credentials content to each of them by reading the process.env, and then exports those. The reason I did not directly type credentials for the environmental variables as I wanted code and data (credentials) to remain separate. Furthermore, I did not want my test credentials to end up in the GIT repository.
Create a custom login Command block for re-usability
To perform programmatic login with the test user's refresh token, I have written a command block named loginByGoogleApi in commands.ts . Now, I can reuse this block as many times as I wish in my test cases.
Cypress.Commands.add('loginByGoogleApi', () => {
cy.session( 'Logging with Google',() =>
{
cy.request({
method: 'POST',//user authentication request
url: 'https://www.googleapis.com/oauth2/v4/token',
body: {
grant_type: 'refresh_token',
client_id: Cypress.env('googleClientId'),
client_secret: Cypress.env('googleClientSecret'),
//performs automatic login which is define for the user
refresh_token: Cypress.env('googleRefreshToken'),
},
})
// (res body) after succesful authentication we recieved an access token for the user
.then(({ body }) => {
const { access_token, id_token } = body
//request to receive user information for the authenticated user by using the previous access token
cy.request({
method: 'GET',
url: 'https://www.googleapis.com/oauth2/v3/userinfo',
headers: { Authorization: `Bearer ${access_token}` },
})
.then(({ body }) => {
window.sessionStorage.setItem('userProfile',JSON.stringify(body))
})
})
},
{
//The validate function is used to ensure the session has been correctly established.
validate() {
cy.window()
.its('sessionStorage')
.invoke('getItem', 'userProfile')
.should('exist')
}
})
})
Explanation
Furthermore, to include a custom command in TypeScript, we need to declare its type. The following code does it.
export {}
declare global {
namespace Cypress {
interface Chainable {
loginByGoogleApi(): Chainable<void>
}
}
}
Write a test suite and add test cases for Google account login
I have written a test suite at spec.cy.tsx.
describe('Google API Login', () => {
beforeEach(() => {
cy.loginByGoogleApi()
cy.visit('/')
})
it('check logged in user', () => {
cy.contains('Moushumi').should('be.visible')
})
it('check logged in user after page refresh', () => {
cy.reload()
cy.contains('Moushumi').should('be.visible')
})
it('logout', () => {
cy.get('#profileNameButton').click()
cy.get('#logoutButton').click()
cy.contains('Login').should('be.visible')
})
})
Inside the block, first, I have included a beforeEach hook that is executed before each test case. For my test cases, I want the test user to be already logged in and browsing the base URL Thus, I have included the following in the hook.
Recommended by LinkedIn
cy.loginByGoogleApi()
cy.visit('http://localhost:3001’)
The test suite consists of three test cases.
1. Check logged-in user
This test case checks that the logged-in user contains the expected test user name.
it('check logged in user', () => {
cy.contains('Moushumi').should('be.visible')
})
2. Check logged-in user after page refresh
This test case checks logged-in user name exists after page refresh.
it('check logged in user after page refresh', () => {
cy.reload()
cy.contains('Moushumi').should('be.visible')
})
3. Logout
This test case checks that the Login button is visible after the test user logs out.
it('logout', () => {
cy.get('#profileNameButton').click()
cy.get('#logoutButton').click()
cy.contains('Login').should('be.visible')
})
Writing Component test cases with Cypress
Component testing involves testing each part or component of a software application separately, with or without isolation from other objects.
Scenario: Test cases for the Navigation component
The navigation component includes header navigation links and shows the logged-in user name. To test these behaviors independently, I write three test cases as shown in the following code block.
Explanation
First, I mount the Navigation component at the beforeEach hook. The navigation component expects two parameters. The first parameter includes the logged-in user's profile information. I provided a mock value for this. The second one is a callback function to set the user's profile information. I provided a Spy function so that I can later check if the function has been called.
There are three test case scenarios in our Navigation component:
1. Compares intended login user name
This test case checks that the profile button in the Navigation component contains the expected initialized test user name.
2. Clicking the profile button fires the event handler
This test case checks that when the user clicks on the profile button, the respective event handler executes to make the logout button visible.
3. Clicking the logout button fires the event handler
This test case checks that when the user clicks the logout button, it triggers the respective event handler. In this case, the event handler executes the Spy function provided at the beforeEach hook. The test case checks that the spy function has been called once with no arguments, i.e., with null values.
Summary
Cypress supports both end-to-end testing and component testing in a single framework. By including appropriate test cases, we can build confidence that the application under test is working as intended. For your convenience, I have uploaded both the test target application and test case at Github
I hope, now you can write your own test cases with Cypress. Happy coding!
Nice post!