How to do Cypress component testing for Angular apps with MSW

Share on facebook
Share on google
Share on twitter
Share on linkedin

In this post, we will cover how to do Cypress Component testing with MSW (mock service worker) and why it’s beneficial to have a mock environment with MSW.

The mock environment

My recommendation for most enterprise projects is to have a mocking environment as it serves the following purposes :

* The front end can be developed independently based on a backend API contract.

* The mock environment can be reused for testing.

The frontend can be developed independently based on a backend API contract

With scrum, often you would develop a feature that includes backend and frontend tasks and the frontend would depend on the backend to work. This dependency from the frontend to the backend can block the frontend work and cause a bottleneck, especially in teams that have dedicated frontend and backend developers (not full-stack).

By having a mock environment, this bottleneck is fixed by the following workflow:

  1. Before you start working on a given feature you do a kickoff meeting agreeing on the backend API contract (API interface, return values, endpoint etc.)
  2. The frontend and backend can now start their work independently
  3. The frontend will use a tool like MSW to create mocked backend responses for running the app in a mock environment and hides this feature behind a feature flag
  4. The frontend will then, regardless of the backend work (because behind the feature flag) deploy the work to production
  5. When the backend is ready and deployed to production the testers can get exclusive access to the feature flag to test it before the rest of the users
  6. When the new feature is tested and accepted the feature flag can be turned on for everyone

This way not only can you test the new feature conditionally in production before your other users but the frontend and backend are not strickly depending on the deployment order and there is no blocked frontend work due to missing backend.

For more about my recommended work processes for efficient sprints in teams, read this post (also you get to see me when I shaved off all my hair during the COVID lockdown lol).

The mock environment can be reused for testing.

The other benefit is you can repurpose your mock environment for testing. Especially when doing component testing as these components would naturally use the whole frontend part and only mock the backend out.

My recommended testing strategy is the high ROI testing strategy (based on Kent C. Dodds testing trophy in the react world) as this maximizes the ROI of your testing by getting the perfect mix between confidence and testing effort by focusing on component testing.

Basically the testing strategy goes as follows:

  • Write a few end-to-end tests for testing the app
  • Cover all your use cases with component tests
  • Cover edge cases and computational logic with unit tests
  • Use eslint and typescript to have automatic rules and type-safety for everything

This gives the infamous testing trophy given the proportional invested testing effort:

So given this heavy focus on component testing it is helpful if you can reuse your mock environment for testing and as as soon as you mount the component you already have test mocks in place making it very easy to write the tests.

For most of your “sunshine” test cases, it would then simply be adequate to reuse the mock environment in your tests and override them if needed.

So let’s see how we can write component tests for a todo app.

Component testing a todo app

To show you all this in practice, I will share with you a todo app, how first the mock environment is set up with MSW, and how it is repurposed for component tests.

The todo app looks like this:

Setting up the MSW mocks

First, we will install MSW:

yarn add -D msw

We need to init MSW as:

npx msw init apps/todo-app/src

This will generate the service worker file mockServiceWorker.js.

We load this mock service worker file in our app build script.

I like to use NX as the build system for almost all my Angular projects (even for non-monorepo projects) so we are also using NX here.

In the project.json we add the mockServiceWorker.js so it will be loaded on init:

"assets": [
	"apps/todo-app/src/favicon.ico",
	"apps/todo-app/src/assets",
	"apps/todo-app/src/mockServiceWorker.js"
],

Now let’s create some mock handlers.

Creating the mock handlers

For the todo app, we create the following mock responses in a todo-handlers.ts file. Also, we keep all our MSW mock-specific stuff in a dedicated library, for this project this is all located in a library at libs/todo-app/domain/mock. Learn more about my architectural approach with Nx here.

First, we create the mock for the get todo items endpoint:

import { http, HttpResponse } from 'msw';

export const MOCK_TODO_ITEMS = [
	{
		id: 'c6893de8-5ea7-4bd5-ab34-b935990abc9e',
		title: 'Steel Ergonomic',
		description: 'Engineer Group instruction set recontextualize Steel',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: 'dd0b94c6-3efd-4a98-b16a-5f396cb842b9',
		title: 'programming Tasty',
		description:
			'interface Intelligent Concrete Soap Stravenue Refined Frozen Towels Heard Island and McDonald Islands',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: '21a59b9e-a9b2-4438-b86b-d2465e717c1d',
		title: 'redefine magenta',
		description: 'Representative Avon indigo local online',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: 'fd6fbccb-f405-40f5-b83d-3e6a6d0cb090',
		title: 'Fresh Handcrafted Steel Pants',
		description: 'enterprise North Korean Won mint green Oklahoma eco-centric',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: '999230a7-4594-4ace-810d-39f72823f3ae',
		title: 'Fantastic Metal Pants solid state',
		description: 'Handmade Concrete Sleek Personal Loan Account sticky',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
] as TodoItem[];

export const GET_TODOLIST_REGEX = '**api/todoList';
export const todoHandlers = [
	http.get(GET_TODOLIST_REGEX, () => {
		return HttpResponse.json([
			{
				result: {
					data: MOCK_TODO_ITEMS,
				},
			},
		]);
	}),
// ...

We use the http.get to have the mock service worker intercept a given request URL regex and return the given HttpResponse.json data. Note, for this project I am using tRPC (from the SPARTAN stack) so I have to envelope the response with [{result: {}}].

The rest of the mocks for the updating operations are even simpler and the full file for the mock handlers looks like this:

import { TodoItem } from '@todo/shared/todo-interfaces';
import { http, HttpResponse } from 'msw';

export const MOCK_TODO_ITEMS = [
	{
		id: 'c6893de8-5ea7-4bd5-ab34-b935990abc9e',
		title: 'Steel Ergonomic',
		description: 'Engineer Group instruction set recontextualize Steel',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: 'dd0b94c6-3efd-4a98-b16a-5f396cb842b9',
		title: 'programming Tasty',
		description:
			'interface Intelligent Concrete Soap Stravenue Refined Frozen Towels Heard Island and McDonald Islands',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: '21a59b9e-a9b2-4438-b86b-d2465e717c1d',
		title: 'redefine magenta',
		description: 'Representative Avon indigo local online',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: 'fd6fbccb-f405-40f5-b83d-3e6a6d0cb090',
		title: 'Fresh Handcrafted Steel Pants',
		description: 'enterprise North Korean Won mint green Oklahoma eco-centric',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
	{
		id: '999230a7-4594-4ace-810d-39f72823f3ae',
		title: 'Fantastic Metal Pants solid state',
		description: 'Handmade Concrete Sleek Personal Loan Account sticky',
		dueDate: new Date().toLocaleDateString('en-us'),
	},
] as TodoItem[];

export const GET_TODOLIST_REGEX = '**api/todoList';
export const todoHandlers = [
	http.get(GET_TODOLIST_REGEX, () => {
		return HttpResponse.json([
			{
				result: {
					data: MOCK_TODO_ITEMS,
				},
			},
		]);
	}),
	http.post('**api/createTodoItem', async req => {
		const newTodoItem = await req.request.json();
		return HttpResponse.json([
			{
				result: {
					data: {
						...(newTodoItem[0] as TodoItem),
						// random id
						id: Math.random().toString(36).substring(7),
					},
				},
			},
		]);
	}),
	http.post('**api/updateTodoItem', async req => {
		const newTodoItem = await req.request.json();
		return HttpResponse.json([
			{
				result: {
					data: {
						...(newTodoItem[0] as TodoItem),
					},
				},
			},
		]);
	}),
	http.delete('**api/deleteTodoItem/**', async req => {
		const { id } = req.params;
		return HttpResponse.json([
			{
				result: {
					data: {
						id,
					},
				},
			},
		]);
	}),
];

I recommend you create a handler file for each domain of the app and expose all the handlers in a handlers.ts file like this:

import { todoHandlers } from './handlers/todo-handlers';
import { generalHandlers } from './handlers/general-handlers';

export const handlers = [...generalHandlers, ...todoHandlers];

Finally, we expose this whole mock setup to the app with a browser.ts file. MSW supports a browser and node setup and we will need to run this in a browser so we have the following browser.ts file:

import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

Setting up the mock environment

In the project.json we will create a mock environment replacement like this:

				"mock": {
					"fileReplacements": [
						{
							"replace": "libs/todo-app/domain/src/environments/environment.ts",
							"with": "libs/todo-app/domain/src/environments/environment.mock.ts"
						}
					]
				}

Which will make us run the app with:

nx serve todo-app -c mock

And have us use the following environment file:

export const environment = {
	production: false,
	mock: true,
};

Then in our main.ts we will check if we are in mock and then load the MSW service worker:

import { worker } from '@todo/todo-app/domain/mocks';

async function enableMocking() {
	if (!environment.mock) {
		return;
	}

	// `worker.start()` returns a Promise that resolves
	// once the Service Worker is up and ready to intercept requests.
	return worker.start();
}

enableMocking().then(() => {
	bootstrapApplication(AppComponent, appConfig).catch(err =>
		console.error(err),
	);
});

We load the MSW service worker first and then run the app.

Writing the tests

For writing the Cypress component tests we have a todo-list.component.cy.ts next to the todo-list.component.ts file.

The test setup

For the test setup, I am a fan of the SIFERS (setup function called first in each test spec) as the SIFERS setup function serves to simplify the test setup before each test case and it overcomes the traditional problems with beforeEach test callback function (eg. needing to provide a config value to a test setup before the component under test is initialized). See more about my high ROI testing strategy here.

import {
	GET_TODOLIST_REGEX,
	MOCK_TODO_ITEMS,
	worker,
} from '@todo/todo-app/domain/mocks';

const setup = (
		{ todoItems }: { todoItems: TodoItem[] } = { todoItems: null },
	) => {
		return cy
			.wrap(
				worker.start({
					serviceWorker: { url: `/mockServiceWorker.js` },
				}),
			)
			.then(() => {
				if (todoItems) {
					return cy.wrap(
						worker.use(
							http.get(GET_TODOLIST_REGEX, () => {
								return HttpResponse.json([
									{
										result: {
											data: todoItems,
										},
									},
								]);
							}),
						),
					);
				}
				return null;
			})
			.then(() => {
				mount(WrapperComponent, {
					imports: [],
					providers: [...appConfig.providers],
				}).then(
					async ({
						fixture: {
							debugElement: { injector },
						},
					}) => {
						const ngZone = injector.get(NgZone);
						const router = injector.get(Router);

						await ngZone.run(() => router.navigate(['']));

						return {
							ngZone,
							router,
							injector,
						};
					},
				);
			});
	};

The idea is the setup function takes in the configuration input needed for the test, handles all the complexity for the test setup without bloating the test cases, and returns the dependencies needed for the test cases (eg. the injector). In this case, the inputs are the todo items to load from MSW overriding the default mock values from the mock environment and it is setting up the app by setting the app’s environment providers (includes core services and routing setup) and mounts a wrapper component for routing.

Also, for allowing the test cases to override the default MSW mock values, we are first starting the MSW mock interception with calling worker.start and then we call worker.use to override mock values.

Making MSW work with Cypress component testing in Angular apps

Note: for a long time Cypress component testing with Angular and MSW didn’t work together due to an infinite loop when loading the MSW. This is fixed in this app by:

Adding devServerPublicPathRoute: '' to the component property in the cypress.config.ts:

import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nx/angular/plugins/component-testing';

export default defineConfig({
	component: {
		...nxComponentTestingPreset(__filename),
		devServerPublicPathRoute: '',
	},
	includeShadowDom: true,
});

and by loading the mock service worker in the test with:

worker.start({
	serviceWorker: { url: `/mockServiceWorker.js` },
}),

Thanks to this Github issue and the latest introduction of the devServerPublicPathRoute property this issue is now fixed.

Wrapper component

The wrapper component looks like this:

	@Component({
		selector: 'app-wrapper-component',
		template: '<router-outlet></router-outlet>',
		standalone: true,
		imports: [RouterModule],
	})
	class WrapperComponent {
		constructor(translateService: TranslateService) {
			translateService.addLangs(['en']);
			translateService.setDefaultLang('en');
		}
	}

Normally this wrapper component will just contain the router setup but can also contain some init logic such as setting the translation language.

Now the test setup is in place, it’s easy to write the tests for the todo app use cases!

Get todo items test

For this test case, we are just reusing and referencing the mock environment values and assert that a todo item is present in the DOM:

	it('should show todo item', () => {
		const todoItem = MOCK_TODO_ITEMS[0];
		setup().then(({}) => {
			cy.get('[data-test=todo-item]').contains(todoItem.title);
			cy.get('[data-test=todo-item]').contains(todoItem.description);
			const formattedDueDate = formatDate(
				todoItem.dueDate,
				'shortDate',
				'en-US',
			);
			cy.get('[data-test=todo-item]').contains(formattedDueDate);
		});
	});

Create todo items test

For creating todo items we init test with an empty todo list and add a todo item (and assert it has been added):

	it('should create todo item', () => {
		setup({todoItems: []}).then(({}) => {
			const title = 'Some title';
			cy.get('[data-test=todo-title]').type(title);
			const description = 'Some description';
			cy.get('[data-test=todo-description]').type(description);
			const dueDate = new Date().toLocaleDateString('en-US');
			cy.get('[data-test=todo-duedate]').type(dueDate);
			cy.get('[data-test=create-todo-submit]').click();

			cy.get('[data-test=todo-item]').contains(title);
			cy.get('[data-test=todo-item]').contains(description);
			const formattedDueDate = formatDate(dueDate, 'shortDate', 'en-US');
			cy.get('[data-test=todo-item]').contains(formattedDueDate);
		});
	});

Update todo items test

For the update test case, we init it with only one of the todo items from the mock environment for simplification, assert it is there, click edit, edit in the form, and assert it has been updated:

it('should update todo item', () => {
		const todoItem = MOCK_TODO_ITEMS[0];
		setup({ todoItems: [todoItem] }).then(({}) => {
			cy.get('[data-test=todo-item]').contains(todoItem.title);
			cy.get('[data-test=todo-item]').contains(todoItem.description);
			const formattedDueDate = formatDate(
				todoItem.dueDate,
				'shortDate',
				'en-US',
			);
			cy.get('[data-test=todo-item]').contains(formattedDueDate);

			cy.get('[data-test=todo-item]')

				.get('[data-test="edit-button"]')
				.click();
			const updatedTitle = 'Edited title';
			cy.get('[data-test=todo-title]').clear().type(updatedTitle);
			const updatedDescription = 'Edited description';
			cy.get('[data-test=todo-description]').clear().type(updatedDescription);
			const currentDate = new Date();
			const updatedDueDate = new Date(
				currentDate.setDate(currentDate.getDate() + 1),
			).toLocaleDateString('en-US');
			cy.get('[data-test=todo-duedate]').clear().type(updatedDueDate);

			cy.get('[data-test=create-todo-submit]').click();

			cy.get('[data-test=todo-item]').contains(updatedTitle);
			cy.get('[data-test=todo-item]').contains(updatedDescription);
			const updatedFormattedDueDate = formatDate(
				updatedDueDate,
				'shortDate',
				'en-US',
			);
			cy.get('[data-test=todo-item]').contains(updatedFormattedDueDate);
		});
	});

Delete todo items test

Finally, for delete, we init with one todo item from the mock environment and assert that it has been deleted after clicking delete.

	it('should delete todo item', () => {
		const todoItem = MOCK_TODO_ITEMS[0];
		setup({ todoItems: [todoItem] }).then(({}) => {
			cy.get('[data-test=todo-item]').contains(todoItem.title);
			cy.get('[data-test=todo-item]').contains(todoItem.description);

			cy.get('[data-test=todo-item]')
				.get('[data-test="delete-button"]')
				.click();

			cy.get('[data-test=todo-item]').should('not.exist');
		});
	});

Conclusion

In this post, we saw how to create a mock environment with MSW that can be used to simplify testing with Cypress component tests. It was very easy to create the tests after we already had mocks we could use.

If you want to learn more about my recommended approach to testing, check out this post.

Also, if you want my best help, I cover topics like this much more in-depth in my course Angular Architect Accelerator and I can give you interactive feedback on your Angular projects over this 8-week training to accelerate your Angular career. You can reserve a seat for the free warmup workshop for the next cohort here.

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

High ROI Testing with Cypress Component Testing

Testing is one of the most struggled topics in Angular development and many developers are either giving up testing altogether or applying inefficient testing practices consuming all their precious time while giving few results in return. This blog post will change all this as we will cover how I overcame these struggles the hard way

Read More »

Announcement: Angular Testing Workshop

I’m announcing a new live workshop: Angular Testing Workshop. It will be half-day workshops over three days, 100% online, where we will learn all the industry best practices with Angular testing so you can apply them in your daily work – taking straight out of my experience with doing Angular testing for big projects. The

Read More »

The Most Common Cypress Mistakes

Cypress has become the preferred way of doing UI testing of Angular apps by many Angular experts. It offers great improvements over Selenium-based testing tools by making the testing experience more like a real user using the built-in retry mechanism of assertions and commands (eg. click on the element), a user-friendly GUI which makes it

Read More »

The Ten Commandments of Angular Development

As a consultant, I normally work with companies between 3-12 months at a time and then I am off to the next gig. Most often, I am hired as a “hands-on” coach, were I am called in for an important and urgent project to make stuff happen within a very tight deadline. This requires that

Read More »