Clear, bug-free code is the cornerstone of a quality product. As the project grows and more code is added, the workload for the quality assurance (QA) team naturally increases as well. End-to-end testing, when integrated with the Cypress test automation framework, can resolve this issue: for example, by requiring less manual effort from your QA team.
In this article, we explore how end-to-end (E2E) testing and test automation with Cypress help reduce the load on your QA team. We also examine Cypressโs key features and benefits and provide a practical example of how we used this framework in one of our projects.
By the end of this article, you will have a better understanding of how the Cypress testing framework can improve the efficiency of your team’s testing process.
Contents:
What are automated and E2E testing?
As a project expands and evolves, QA specialists need to test new user scenarios and verify previously implemented and tested functionality. This means that the QA team has to perform regression testing of the entire project or its affected parts. These activities can take up a lot of the QA teamโs time, decreasing the quality of work and delaying your productโs release.
Thatโs why automated testing is an integral part of developing high-quality applications. Automated testing, including end-to-end tests, allows the QA team to focus on critical processes and accelerate the overall development cycle. End-to-end (E2E) tests verify how a system works as a whole, from data entry to result output, by recreating real usage scenarios. E2E tests also allow you to detect issues that may be missed in other types of testing:
- Problems with server settings
- Unexpected database errors
- Issues with displaying information in the user interface
By implementing E2E tests, QA teams can streamline and automate the process of validating user scenarios and checking overall application functionality. With well-designed and relevant E2E tests in place, your team can make sure that new features and previously tested functionalities work seamlessly together, reducing the need for repetitive manual regression testing.
However, when selecting a tool for test automation, you should exercise caution and carefully evaluate the options. In the next section, we show a practical experience of choosing a relevant automated testing framework and applying it within a web application development project.
Introducing E2E testing into our project
In one of our recent projects, we worked on developing a CRM system. While the client already had a foundation for this system, we were challenged to add multiple new features.
Due to the quick project start and the large number of features to be added, the initial development phase became chaotic. Developers had to maintain complex processes within the application, and testers had to conduct more and more regression testing of them.
We came up with a system where testers would write test cases for each feature. Before this, the developers already knew and wrote end-to-end tests that fully covered the systemโs functionality after implementation. Even though plans for implementing E2E tests were not initially outlined, we quickly realized that this approach would speed up the release cycle and improve test quality. We could be confident that with each new code update, nothing would break, as each developer could run all tests on their own in a matter of minutes.
We decided to implement automated testing in this project and needed to replicate the same user actions as in manual testing. Initially, we were choosing between two popular frameworks: Selenium and Cypress for E2E testing.
Cypress is a free modern JavaScript framework for automating web application testing. Cypress automation testing allows developers to write tests that simulate real user scenarios and user interactions with the application, helping to make sure that the application works as expected. In addition, Cypress has detailed documentation and an active community, which can help your QA team with deploying and using this framework.
Selenium is an open-source framework that allows for automated web application testing across different browsers and platforms. It also gives developers an opportunity to write tests in their preferred language because of its wide range of programming language bindings.
While Selenium and Cypress are both popular and useful automation testing frameworks, they have some differences in their approach and features:
As you can see, Selenium supports multiple programming languages and browsers, making it more flexible. On the other hand, Cypress E2E testing is faster and more user-friendly, with a straightforward testing approach and a built-in debugger.
To choose which solution would work best for our project, we first outlined the core requirements for the automated testing framework within this project:
- Browser execution. The framework had to support running directly in the browser for seamless testing within the environment.
- Test logging. It was important to have test logging at all stages of execution, preferably with screenshots.
- Built-in toolset. The framework should come with all necessary tools out of the box, without the need to install multiple libraries.
- Azure DevOps integration. It was also important to be able to integrate with Azure DevOps CI/CD, which would allow us to automatically test and deploy our applications.
- Browser compatibility. According to the initial project requirements, the set of browsers to be tested only included the most popular: Chrome, Firefox, and Edge.
Also, it was important for us to choose a framework that allowed us to emulate user behavior even during manual testing. The chosen framework should work directly within the browser and include logging functionality at all test execution stages, preferably with screenshots.
Thanks to its architecture, which allows for direct testing of applications in the browser, Cypress fits all project requirements. Selenium, on the other hand, uses WebDriver to communicate with the browser from the outside, which was not suitable for our project requirements.
Now, letโs explore some of the key features of Cypress to see how you can use them in your project.
Key features of Cypress
What sets Cypress apart from other testing tools is that it operates in the same run loop as the application instead of operating outside of the browser and executing remote commands across the network. Cypress automation framework is executed on a NodeJS server. So Cypress and NodeJS processes can be run on the same session, which allows Cypress to mock JavaScript global objects.
Since Cypress is installed locally on your machine, it can access and utilize different functionalities provided by the underlying operating system to automate tasks such as:
- Taking screenshots
- Recording videos
- Performing file system operations
- Executing network operations
Cypress offers a unique advantage by granting you access to the applicationโs front end and back end. This allows Cypress to respond effectively to real-time events while operating outside of the browser for higher-privilege tasks.
Letโs take a look at the core features of Cypress that you can use during the development and testing of your web application:
During our project, we actively used the time-traveling feature, which helped us with debugging, as it allows developers to identify the exact moment when an error occurs and view the state of the application at that point. This information makes it easier to identify the root cause of issues and fix them quickly.
Next, letโs take a look at some of the Cypress test automation framework features that make the web application development process effective and convenient.
Intercepts
The Cypress intercept command is a powerful feature that allows you to intercept and modify network requests and responses from your web application. This feature is useful for testing scenarios that involve dynamic content or data that changes over time.
With Cypress intercepts, you can intercept commands and modify their behavior to simulate specific scenarios, thus making the testing process more efficient, as you save time on setting up presets and returning pre-existing data to the tested pages.
To intercept and control network requests made by your application during testing, use the cy.intercept()
command. You can also specify custom responses to this command and simulate different behavior scenarios.
Reporting
Cypress provides various built-in reporting options that can help you build customized reports, analyze the results of your test runs, and identify any issues that may arise. Here are some of the reporting options provided by Cypress:
- Command log: Captures every command executed during a test run and logs them to the command log. You can access this log from the Cypress Test Runner UI and observe what Cypress is doing during the test run.
- Test Runner UI: Provides a graphical user interface (GUI) that shows the status of each test and provides access to various logs and videos that help in debugging test failures.
- Screenshots and videos: Captures screenshots and videos of the test run, which can be used to identify issues and debug failures.
- Dashboard service: Provides a cloud-based dashboard service that allows you to review and analyze your test runs. The dashboard service provides detailed reports and analytics, including test results, video recordings, and error messages.
- Custom reports: Allows you to create custom reports using various plugins and libraries. This way, you can gather and analyze specific information based on your projectโs requirements.
CI/CD pipeline and error detection
Logging, screenshots, and videos of all Cypress test results are essential tools for reducing the time spent on detecting and verifying errors. You can configure the passing of tests and sending of screenshots and videos of failed tests directly to the CI/CD pipeline. This way, both QA specialists and developers can see the specific steps that caused the error and understand whether it was an application error or a system failure.
Logging is extremely useful when developers write tests that work locally but fail in different environments. This is where the pipeline serves as a shield against such critical errors. Anyone on the team can access it, see failed tests and test names, take or review screenshots and videos, and create a corresponding ticket. The entire process takes just a few minutes.
As a result, developers and QA specialists constantly have fresh testing results, and the QA team can devote more time and attention to checking new functionality and finding non-trivial errors.
Now, letโs take a look at how we used Cypress to reduce the load on our QA team in the project we mentioned earlier.
Case study:
5 Best Automation Testing Tools for Software Engineering: Which to Choose for Your Project?
Implementing Cypress into our test development process
Initially, our Cypress tests were similar to manual tests in terms of performed actions and execution results. However, as we continued to use the framework, we started to create tests that used request interception and browser state setup without the need for any external components.
As a result, tests no longer relied on preliminary data preparation. Data was created and provided directly for each test case: for example, to test the behavior of a table on a page when it contains no data. This led to shorter test execution times since we no longer needed to create data before or during tests. It also prevented us from getting too many false positives if someone forgot to prepare a new dataset beforehand or if data preparation failed.
At the request of our QA team, we created datasets to perform boundaryโvalue analysis, which is difficult to test in real-life situations without continuously cleaning up the database prior to doing something. We covered the primary regression testing scenarios using test cases created by the manual testing team. We also expanded the list of tests to include errors found during manual testing and created automated tests for new functionality shortly after its implementation.
For example, here are some test cases from the early stages of our project:
1. Login test group:
- Open the website, log in using valid credentials, and check the current URL in the browser. It must have โ…/dashboardโ in it.
- Open the website, but donโt enter anything and try to log in. Note the pop-up error window that states that data needs to be entered into the form.
- Open the website, enter incorrect data, and try to log in. Note the pop-up error window stating that the login data is incorrect.
2. Logout test group:
- Open the website, log in, log out, and check the current URL in the browser. It should contain โ…/login.โ
- Open the website, log in, log out, try to go to โ…/dashboardโ and make sure that it isnโt possible and that you are redirected back to the login page.
3. Dashboard page test group:
- Log in and make sure that the dashboard page displays the expected elements.
Letโs see what the process of developing a test from scratch looks like.
First, we need to create specifications for each test group that is separated either logically by a specific feature or physically by a specific file in the project. To do this, we can create specification files either directly in a specific folder or through the Cypress interface.
Now letโs write the tests.
Login test group
login.cy.js
describe('Login / Logout functionality', () => {
describe('Login', () => {
beforeEach(() => {
cy. visit('/');
});
it('should display an error message if username or password is missing', () => {
cy. get('form[name="loginForm"]'). submit();
cy. on('window:alert', (message) => {
expect(message). to. equal('Please fill in both fields.' );
});
});
it('should display an error message if username or password is incorrect', () => {
cy. get('input[name="username"]'). type('invalid');
cy. get('input[name="password"]'). type('invalid');
cy. get('form[name="loginForm"]'). submit();
cy. on('window:alert', (message) => {
expect(message). to. equal('Invalid username or password.' );
});
});
it('should redirect to the dashboard page if username and password are correct', () => {
cy. get('input[name="username"]'). type('admin');
cy. get('input[name="password"]'). type('password');
cy. get('form[name="loginForm"]'). submit();
cy. url(). should('include', '/dashboard');
});
})
Logout test group
describe('Logout', () => {
beforeEach(() => {
cy. visit('/');
cy. get('input[name="username"]'). type('admin');
cy. get('input[name="password"]'). type('password');
cy. get('form[name="loginForm"]'). submit();
cy. visit('http://localhost:3000/dashboard');
cy. get('.navbar-toggler'). click();
});
it('should log the user out when "Logout" link is clicked', () => {
cy. get('a[href="/logout"]'). click(); // Click on the "Logout" link
cy. url(). should('include', '/login'); // Verify that the user is redirected to the login page
});
it('should not allow access to dashboard page after logout', () => {
cy. get('a[href="/logout"]'). click(); // Click on the "Logout" link
cy. visit('http://localhost:3000/dashboard'); // Visit the dashboard page again
cy. url(). should('include', '/login'); // Verify that the user is redirected to the login page
});
})
});
Dashboard page test group
dashboard.cy.js
describe('Dashboard functionality', () => {
beforeEach(() => {
// Log in as the test user
cy. visit('http://localhost:3000/login');
cy. get('input[name=username]'). type('admin');
cy. get('input[name=password]'). type('password');
cy. get('button[type=submit]'). click();
cy. url(). should('eq', 'http://localhost:3000/dashboard');
});
it('should display the dashboard page after successful login', () => {
// Verify that the dashboard page is displayed
cy. contains('h2', 'Dashboard'). should('be.visible');
cy. contains('p', 'Welcome to the dashboard!' ). should('be.visible');
});
it('should display the navbar with links to other pages and logout link', () => {
// Verify that the navbar is displayed with links to other pages
cy. get('nav'). should('be.visible');
cy. get('a.navbar-brand'). should('have.text', 'Dashboard');
cy. get('.navbar-toggler'). click();
cy. get('ul.navbar-nav'). should('be.visible');
cy. get('ul.navbar-nav'). within(() => {
cy. get('li'). should('have.length', 4);
cy. get('li'). eq(0). should('have.class', 'active'). find('a'). should('have.text', 'Home');
cy. get('li'). eq(1). find('a'). should('have.text', 'Profile');
cy. get('li'). eq(2). find('a'). should('have.text', 'Settings');
cy. get('li'). eq(3). find('a'). should('have.text', 'Logout');
});
});
});
Letโs navigate to the Cypress interface and run the login specification. For example, letโs consider a test for successful login.
As we can see, on the left side of the screen, there is a report on the step-by-step progress of the test, and on the right side, there is a browser in which the progress is demonstrated visually.
With these tests, we have covered a small part of the website logic (the login functionality). Covering the login functionality might take some time, but by doing this, we can be sure that any changes made by developers donโt have a negative impact on the project. In addition, covering the login functionality completely frees manual test engineers from this type of login verification. Although our project could be deployed multiple times a day, having automated tests enables us to quickly receive reports on errors and start fixing them right away.
Pros and cons of using Cypress for automated testing
The biggest benefit of working with Cypress was that this framework allowed us to automate manual tests, saving a lot of our QA teamโs resources.
For example, in one of the new features, we had a huge settings page with dozens of controllers. This was a page with lots of controllers, choosing one of which impacted the behavior of other pages. In general, this was kind of a template processor. The test case required the following:
- Logging in
- Navigating to the settings page
- Opening five pages in adjacent tabs
- Making a preset
- Checking the changed state of the opened pages
Letโs say that you need to test 20 different preset configurations.
A manual check of just one such variant typically takes 10 minutes of work. A QA specialist needs to carefully configure the controllers under the preset and meticulously verify the final states of the pages.
Automated testing of one configuration, on the other hand, takes approximately one minute.
As a result, we have the following results:
You can see that using the Cypress framework clearly reduces the load on QA and development teams in the long run.
Note: This example shows the overall capabilities of the Cypress framework. Your results of switching to automated testing may differ depending on the specifics of your project.
Of course, even though Cypress was a better choice for our project, it also has some drawbacks that have to be taken into account. For example, more advanced Cypress features might require more expertise from your QA and development team.
You also need to have an effective test writing strategy in place when dealing with a large-scale project. In our case, testers were responsible for test cases, and developers wrote the tests.
Conclusion
Cypress simplifies the process of testing web applications by providing a powerful and intuitive testing framework. Its ability to interact with a web application in a browser instance and its extensive set of built-in commands make this framework a popular choice for end-to-end testing. Building automated tests with Cypress can help your team free up time from the routine of manual regression testing.
At Apriorit, our QA teams have extensive expertise working with automated and manual testing frameworks. We can assist you with optimizing the workload for developers and QA engineers while ensuring high code quality and speedy project delivery.
Reach out to start working with us on the best QA strategy for your project together!