This article aims to describe how you can set up end-to-end testing for Angular with Cypress including TypeScript. You will write your very first e2e tests and make them ready to run on a CircleCI as a continuous integration system with every update to your repository.
Before we start: What is an e2e test?
End-to-end (short e2e) testing is a type of software testing that validates the software system along with its integration with external interfaces. The purpose of end-to-end test is to exercise a complete production-like scenario.
I have a frontend development background in Microsoft's .NET & WPF and remember the times where we evaluated costly frameworks to write end-to-end tests for our projects. After a lot of evaluations and weeks, even months of custom glue code and development of test infrastructures on top of existing tools, we finally got some e2e tests running. They were brittle, often failed because of manual adjustments we had to do or problems with flaky runners in the continuous integration pipeline.
Some years later with Angular and Protractor as a default for e2e tests, we were still based on page objects, Selenium Web Driver and the tests continued to be rather unreliable. No expensive commercial frameworks and custom infrastructure were needed. But was it fun to write e2e tests? No.
However we are in 2020 now and time has come for new heroes to arise. 🚀
What is Cypress?
Cypress promises fast, easy and reliable testing for anything that runs in a browser. It is not based on Selenium Web Driver which is using network connections to interact with your browser. Instead Cypress is a test runner that runs inside your browser next to your web application and therefore has has direct control over it.
Without going into all the details, this not only makes Cypress faster and more reliable, it also opens the door for a lot of other interesting features like
time travel debugging,
easy snapshotting and recording,
automatic waitings.
On top of all the features, Cypress has a developer experience (DX) that is nearly unrivalled. Have you ever seen a message in the error logs of your failed build that tells you exactly what you did wrong, points you to the right dependencies to add and also links to an explanatory documentation site describing the problem? This is what Cypress feels like. It is built by developers for developers.
Hereafter, we will install Cypress for a fresh Angular project created with the CLI. We'll write some e2e tests and conclude with running these by an automated build system. All these steps should not take more than 60 minutes. We try to keep the steps as short as possible, leveraging existing tools like Angular Schematics, libraries and common templates.
Prerequisites
This guide assumes that you have a standard Angular 9 app project. If not, you may create one like you would normally do with the Angular CLI. If you don't have the CLI installed globally, you can make use of the npx command that will install it temporarily on the fly:
npx @angular/cli new <app-name>
Setting up Cypress
In order to set up Cypress together with TypeScript as fast as possible, we make use of an existing schematic developed by BrieBug.
In the root of your Angular project, you can open the terminal and enter the following command:
ng add @briebug/cypress-schematic --addCypressTestScripts
If the CLI isn't installed globally, the ng command may not be available directly. You can enforce the use of the local ng from the package.json:
npm run ng -- add @briebug/cypress-schematic # In case 'ng' could not be found
We can safely remove Protractor because it will be completely replaced. During the installation some binaries were downloaded because Cypress comes with an Electron-bundled UI as an interactive test runner.
Using the flag --addCypressTestScripts two handy npm scripts were added to make the work with Cypress more comfortable. One to run e2e tests headless and the other script running the tests with the Cypress UI runner:
If you were to run one of these scripts standalone, the test would fail because it tries to route to http://localhost:4200 where nothing is served at the moment. In order to fix this, we need to open a second terminal and serve our Angular application beforehand with npm start.
Luckily the schematic adjusted the e2e command so that this is done for you automatically by the CLI builder. You can serve the application and start the e2e test by using the following command:
npm run e2e
Cypress will detect that we launched it for the first time. It verifies its installation and adds some initial example files. After the UI has opened up, we can see a test that has already been created for us.
Selecting the test will run it. Initially the test will fail because we didn't actually test something properly. We will fix this now.
Writing Some Tests
As a very first step, as proposed by the Cypress best practices, we set our global baseUrl, so that we don't have to duplicate this on every test execution. Add the following to the configuration cypress.json:
//cypress.json{"baseUrl":"http://localhost:4200"}
After that, we write our very first smoke test that only checks whether the default app title is set on the Angular starting page. Therefore, change the content of the spec.ts to the following content:
// spec.tsit('smoke test',()=>{cy.visit('/');cy.contains('cypress-e2e-testing-angular app is running!');});
The test starts by routing to our baseUrl and proceeds by querying any element that contains the text cypress-e2e-testing-angular app is running!.
Testing User Flows
This test should already work, but let's write some more interactive ones. As e2e are inherently slower than unit tests, it is totally fine to have e2e tests that model the entire user flow for a feature.
For example, we want to check whether some characteristics of the starting page are valid: Our page should contain the title and the ng generate text in the terminal by default, but when the users clicks the Angular Material button, we want to ensure that the proper ng add command is displayed in the terminal view below.
You may replace the content of your test file with this:
// spec.tsdescribe('When Angular starting page is loaded',()=>{beforeEach(()=>{cy.visit('/');});it('has app title, shows proper command by default and reacts on command changes',()=>{cy.contains('cypress-e2e-testing-angular');cy.contains('.terminal','ng generate component xyz');cy.contains('Angular Material').click();cy.contains('.terminal','ng add @angular/material');});});
We refactored our test suite by adding a describe block to capture all tests that run when the starting page is loaded. As we visit the baseUrl every time, we moved this into the beforeEach call. Lastly we combined the basic smoke tests with the test for the terminal view on the starting page.
It is important to know that you should not store Cypress' query results in variables, but instead work with closures. Moreover, we selected elements by CSS classes and text content, which may be too brittle. It is recommended to use data- attributes for selecting elements.
Cypress has a lot of great features and possibilities. We won't cover all of them because our goal is to focus on the very first starting point. The official documentation is really good and covers everything on how to interact with elements.
If you rerun this test suite, you should see the UI clicking through each scenario and all three tests should pass this time. ✔✔✔
Setting up Continuous Integration
Now that our tests run locally, let's kick of a small CI (continuous integration) pipeline. A good way to prepare for this, is to create npm scripts and combine them so that the build system can use a single script as entry point. By following this method, you can try the CI steps locally before pushing online. Moreover npm scripts are rather independent from any actual build system.
On CI, we need to start our server in the background and wait for it to bundle our application, which might take a while. Then we need to start the Cypress test runner, go through the tests and shut down the server when the tests finish. Luckily we can do this all with a single utility called start-server-and-test as described in the Cypress docs:
npm install--save-dev start-server-and-test
After this is installed, we use the Angular serve which is currently behind npm start and combine it with the headless cy:run command:
You could surely use a production build or build beforehand and serve the app using any http server. For sake of conciseness, I will leave these improvements up for you.
Circle CI
For our example, we choose CircleCI because it integrates very well with GitHub, is commonly used there and has a free plan. You may use any other CI system like Jenkins or GitLab (which I have the most experience with). After signing into CircleCI and connecting to our GitHub account, you can select the repository and create a new project via their dashboard.
In order to configure the pipeline, you could write a config.yml by selecting a template and adjusting it to your needs and eventually running the e2e script. Fortunately Cypress has a ready to use configurations (called Orbs) for CircleCI which already include the installation of dependencies, caching and so on. Before we can use it, we must visit the Organisation Settings to enable third party runners.
# circleci/config.ymlversion:2.1orbs:# This Orb includes the following:# - checkout current repository# - npm install with caching# - start the server# - wait for the server to respond# - run Cypress tests# - store videos and screenshots as artifacts on CircleCIcypress:cypress-io/cypress@1workflows:build:jobs:-cypress/run:start:npm startwait-on:'http://localhost:4200'store_artifacts:true
The pipeline only has one job: Run all e2e tests. It checks out the current branch, installs all dependencies including caching, starts the application server and runs our tests. Additionally, videos (recorded by default) and screenshots (in case tests are failing) are uploaded as CircleCI artifacts for further inspection.*
Conclusion
The steps in this guide are rather minimal. You may use your existing Angular project, may change the configuration of your Cypress test suites and write a lot of more meaningful tests. Moreover, you may define npm scripts for different scenarios and environments and of course your entire build pipeline may be extended with linting, unit testing, building and even deploying your application. Nevertheless, this should be a first step which shows how quick automated end-to-end tests can be set up in nowadays.
Wait till you write real Cypress tests for your application. You will have fun!
I hope that you will also find some value in this article. If you have any questions or remarks, just let me know. Your feedback is very welcome!
You can find the sources for this guide on GitHub:
Example Angular 9 app using Cypress for end-to-end testing.
CypressE2eTestingAngular
This project was generated with Angular CLI version 9.0.6.
Development server
Run ng serve for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.
Code scaffolding
Run ng generate component component-name to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module.
Build
Run ng build to build the project. The build artifacts will be stored in the dist/ directory. Use the --prod flag for a production build.
*For other CI systems we could use our previously defined npm script. However we need to take care of all the additional work by ourselves. If you already have an existing sophisticated pipeline it could be easier to integrate just the script.