Let's keep this introduction brief. I had a lot of things on my mind after reading this fantastic article by Filip Hric about how to structure a big project in Cypress.
I concur with some, disagree with others. His article covers a lot of ground despite being very brief. Hence, I'd want to start with a topic that is important to me: his actions sequence methodology. I'm hoping that this article may serve as a dialogue opener in some way. So let's get started!
Beginning
To begin with, let's address some fundamental questions regarding test writing, such as the available approaches, the separation of logic from pages, and the existence (if any) of "bad" techniques. Regarding the first question of whether we should divide our testing project into complex logic, while it's not mandatory, it can greatly simplify the lives of devs/QAs. Adopting a standard method for writing test logic is crucial for ease of writing, reading, and managing tests in the long run, especially for larger projects, as mentioned in Filip's article.
Methodologies / design patterns
Ok, you convinced me. What I can do? What I can slap into project and what will work for me?
Numerous effective methods exist for organizing test logic (I'm not referring to organizing tests or the file structure for helper methods; this can be covered in another article as it's also a rather big topic). The optimal technique relies on factors such as the project's pace, team size, and what works best for the team. However, industry consider the following recommendations as the "golden trio" of methodologies used to maintain tests.
Page object model (POM)
Page Object Model (POM) is a design pattern used to organize and maintain test automation code. In this methodology, each page of the application under test is represented by a separate class or module containing all the necessary elements and methods to interact with that page.
Advantages:
- POM promotes reusability of code by keeping page-specific code separate from test code
- It makes tests more readable and easy to maintain by encapsulating page functionality into separate classes
- It helps in minimizing code duplication and improves test maintainability
- It enhances test scalability as new pages can be added to the framework with ease
Disadvantages:
- It may not be the best approach for small-scale applications with a limited number of pages, as its designed for large and complex applications
- This methodology can be memory (RAM) demanding in more complex test setups, because it loads entire page classes and init (so also store them in-memory)them during the test configuration phase
App actions
App Actions is a well-known Cypress design pattern that allows for the creation of tests at a higher degree of abstraction. Rather than engaging with specific components on a page, tests are built by describing a sequence of user activities that mimic user behavior from any point in the app. It's because it uses public app methods to configure tests to our specifications before the test is done.
Advantages:
- App Actions increase test code readability and maintainability by abstracting low-level UI interactions to independent methods
- Enables us to prepare the app for testing purposes
- It decreases the complexity of test code and makes tests less prone to failure due to small UI changes
- It saves time in test creation and debugging as tests can be written faster with less boilerplate code like in POM
Disadvantages:
- It requires a good understanding of the application's user flows and may be more challenging for new devs/QAs/testers to learn
- It requires a good knowledge how the app is built, and how to expose certain functionalities of the app. Or it needs assistance from the dev's team
BDD via Cucumber
Behavior Driven Development (BDD) is a software development methodology that emphasizes collaboration between developers, testers, and stakeholders. Cucumber is a popular BDD tool that enables writing tests in a natural language format that can be easily understood by non-technical stakeholders.
Advantages:
- "BDD via Cucumber" promotes collaboration between different teams and improves communication between developers, QAs, testers, and stakeholders
- It improves test traceability and provides better coverage of requirements
- Easy to understand tests building blocks
- It helps in identifying defects early in the development cycle, as cases usually written much earlier than in other methodologies
Disadvantages:
- It requires additional setup and configuration to use Cucumber with Cypress
- It may not be the best approach for small-scale applications with a limited number of stakeholders (burning resources)
- It may require additional effort to maintain test code as requirements change
Hey, but Filip mentioned "actions sequence", where does it fit? It's not in your list. I thought it's main reason why this article even exists!
As far as I understand Filip's article, he's writing about mix, of app actions with "native" BDD of Cypress, and idea of page object model. So we operate on certain page... with usage of native Cypress commands, in BDD manner.
Actions sequence is great... but
I was surprised that someone is doing actions sequence. I'm (with colleagues) already using it across projects, much before the article showed up.
But first of all, as before, let's dive into, what it exactly is, its advantages and disadvantages.
Actions Sequence is a test automation methodology that combines the principles of BDD, Page Object Model, and App Actions. In this methodology, tests are written as a sequence of user actions that operate on a particular page/view, using the native Cypress commands. Unlike POM, this methodology doesn't define page objects as classes, but instead, it focuses on defining separate actions on pages/views that can be reused across tests.
Advantages:
- Actions Sequence improves test reusability and maintainability by abstracting page interactions into separate actions
- It promotes a high level of readability and maintainability of test code
- It saves time in test creation and debugging as tests can be written faster with less boilerplate code
- It combines the best of both worlds: BDD for almost natural language test writing and App Actions for higher-level interaction with the UI
- Eliminates POM's memory issue (but does it really? I mean in terms of Filip's article... I'll write about it later in this article)
Disadvantages:
- Test framework is much more modular.
- There might be question "Why it's a problem, doesn't it bring flexibility, so it's advantage?" - let me explain my thought process:
- Yes, a more modular test framework with different actions for pages/views can improve test reusability and make the test code more maintainable from a mid/senior standpoint. It can also make the structure of the test framework easier to grasp and reduce code duplication.
- However, the modular nature of the framework, can be more challenging for newcomers and necessitate a higher level of understanding to navigate. They may be more comfortable with Cypress Commands than with designing different actions for pages/views. Maintaining the correct folder/file structure might sometimes be difficult for novice devs/QAs. This series is designed for beginners who wish to break out of their shells.
- Increased likelihood of code duplication by dev/QA who don't understand the structure of the test framework
- It necessitates a thorough understanding of the application's user flows and may be more difficult for inexperienced testers to master
- It might not be the best solution for super small-scale applications with a modest number of pages
If you think it's working (so it must be great), why you used "but" in article's title?
It wasn't "but" in meaning "I'm against it". It was because I work with such approach for some time, I think I may have some ideas and approaches which I think can be... filling out some gaps in such approach Filip showed up in his article.
Selecting elements
I strongly agree that using the testing attribute, such as 'data-cy' in the article's example, is beneficial. However, I have a concern regarding repeating these attributes. It's possible that the author only intended to demonstrate the process and avoid delving into the selection process. Nevertheless, I recommend a personalized approach to selecting elements on the page by separating and customizing them. I have already detailed this process in a "how-to" article, which can be found here.
Using Cypress Commands
As previously stated, this method has the benefit of being lightweight and using less RAM. However, the example provided by the author involves creating new Cypress Commands for every action, which I believe could result in even more memory usage than the Page Object Model.
To illustrate, let's consider a scenario where we have two pages. With the current approach, all actions would be written as Cypress Commands. This means that when we want to test something on just one page, we end up loading the entire hefty Cypress API into memory, even though we only use a portion of it for the test (excluding the literal Cypress API operations). So, how would I go about it?
Actions could be carried with using modular approaches.
Instead of:
// cypress/commands/actions.ts -> imported to cypress/commands/e2e.ts
Cypress.Commands.add('pickSidebarItem', (item: 'Settings' | 'Account' | 'My profile' | 'Log out') => {
cy.get('[data-cy=hamburger-menu]')
.click();
cy.contains('[data-cy=side-menu]', item)
.click();
});
// cypress/e2e/app1/some-test.spec.ts
describe("Sample", () => {
it("should pick sidebar item", () => {
cy.pickSidebarItem("Settings");
cy.url().should("include", "/settings");
});
});
I'd suggest such approach:
// cypress/e2e/app1/utils/actions.ts
export const pickSidebarItem = (item: 'Settings' | 'Account' | 'My profile' | 'Log out') => {
cy.get('[data-cy=hamburger-menu]')
.click();
cy.contains('[data-cy=side-menu]', item)
.click();
});
// cypress/e2e/app1/some-test.spec.ts
import { pickSidebarItem } from 'utils/actions'
describe("Sample", () => {
it("should pick sidebar item", () => {
pickSidebarItem("Settings");
cy.url().should("include", "/settings");
});
});
Thanks to this we divided responsibilities of methods. Actions are kept close to app/page/view we test. Also from my point of view, Cypress Commands should expand usability of Cypress API (like new way of selecting elements), not adding specific interactions or steps that a user would take within an app (or to setup tests for testing).
Clutter of Cypress namespace
This is something completely separate from the second point I made earlier. If we don't use Cypress Commands, we don't have to clutter up the Cypress namespace. But guess what? We can still make chainable methods (and give them types). Isn't that great?
From this:
// cypress/commands/actions.ts -> imported to cypress/commands/e2e.ts
declare global {
namespace Cypress {
interface Chainable {
addBoardApi: typeof addBoardApi;
}
}
}
/**
* Creates a new board using the API
* @param name name of the board
* @example
* cy.addBoardApi('new board')
*
*/
export const addBoardApi = function(this: any, name: string): Cypress.Chainable<any> {
return cy
.request('POST', '/api/boards', { name })
.its('body', { log: false }).as('board');
};
to this
// cypress/e2e/app1/utils/actions.ts
/**
* Creates a new board using the API
* @param name name of the board
* @example
* addBoardApi('new board')
*
*/
export const addBoardApi = function(this: any, name: string): Cypress.Chainable<any> {
return cy
.request('POST', '/api/boards', { name })
.its('body', { log: false }).as('board');
};
So, to sum it up, Filip came up with this cool way to blend App Actions with native BDD from Cypress and the Page Object Model concept. It's pretty awesome, but there's room for customization to make it even better for our specific needs. So, what do you think? Do you have any ideas for how we could tweak it to make it work even better for us? Open to discussion!
Top comments (6)
Hey @Tymoteusz Stępień,
I just finished reading the whole article, and honestly, I think Filip’s ideas fit much better for complex projects.
Some of your code examples are just harder to read than Cypress native commands. An example is the picksidebar one.
By the way, why not use Custom Commands for “app actions”? Then you don’t need to pollute your tests with imports of multiple pages. It shouldn’t be an issue to use a Cypress command to pick a sidebar item. Why not? This is why it’s designed the way it is.
Also, regarding less experienced devs/QAs not understanding modular code. This can be solved with documentation and mentorship. This is what seniors should be doing, besides coding. I mean, we should be helping them to become seniors too. We should give the example. By the way, if certain module feels complex, maybe it is. Shouldn’t we (the seniors) be making them simpler then?
You mentioned a lot about memory consumption. IMO, this should not be an issue in an era where one can have as much computational power as needed, accessible via a web browser with internet access. Also, we can make use of preprocessors for optimizing our code, and make it lighter. Also, we should be writing independent tests. Tests that could be executed in parallel. And even if they consume a lot of memory, they would still give fast feedback to the teams, just because we wrote them in a way where they can benefit from parallelization.
Instead, we should be focusing on: how fast can we make the tests ru ? How independent can they be? How easy to change they are? *How easy to throw away are? *🧐 How well documented are they?
Cypress is bringing new paradigms to e2e testing, paradigms already used for a long time in other kinds of tests (e.g., unit tests.) Why not benefit from them too?
It should be our responsibility to learn about them and evolve what we know (and do) about testing.
I love Cypress’s slogan.
“The web has evolved. Finally, testing has too.”
Finally, when I read (or hear) design patterns, I think about the ones described by The Gang of Four (Design Patterns: Elements of Reusable Object-Oriented Software g.co/kgs/Xjevab). We should use a different terminology to avoid ubiquity. I suggest “Testing Patterns.
By the way, thanks for the article. It was a good reading.
I hope to hear more from you.
Thank you for your comments and observations.
You make some good points, and I respect your point of view.
You are correct that using custom commands for app activities is a feasible option to avoid importing several methods into tests. But, I feel it is dependent on the application's size and complexity. Yes, for given example of simple "one short method, testing literally nothing" it may be feasible as you pointed out (and even like single-page app). But given larger set of views/pages... Or even apps in monorepo containing few of them (as they can share some actions for tests setup)... it would in the end bite your a**. Telling this from my own experience. But if it works for you... and you feel comfortable, I have nothing to say against it.
I'd also like to point out that importing multiple methods into your tests isn't necessarily a bad thing (because we use what Cypress gives us, BDD-like form in these methods, simply by changing Cypress Commands to external methods), as long as the methods are well-organized and their responsibilities are clearly defined.
Mentorship and documentation, like you stated, might also help less experienced engineers and QA comprehend modular code better. So yes, true, I might add this as point. Or just people would refer to this discussion for it.
About usage of Filip's idea for this methodology in complex projects, I thought that I sent message that it can work. It may be more painful for some folks (myself included 😅 one of the reasons why I wrote this article) as stated above... but surely it will work!
While current computers (mainly now we'd talk about cloud computing) have more memory accessible than in the past, it's still vital to develop efficient and optimized code, especially for bigger test suites that may run in parallel. Although preprocessors and independent tests might assist, it's still necessary to be careful of memory utilization and design efficient code whenever possible. Money don't grow on trees, SRE thinks about it when calling for bigger runners and C-levels thinks about those money leaks too, so they already push optimization, so why we (as QAs) shouldn't? As QAs we should also push for optimizations wherever we can - it's also part of quality assurance, isn't?
Overall, I agree that Cypress is introducing new paradigms to end-to-end testing, and we must be willing to learn and evolve our testing procedures in order to take advantage of these new techniques. But also we should remember that regardless of the tools and methodologies used to build tests, the objective is to always strive for tests that are rapid, independent, easy to update, and well-documented. Thank you once more for your insights!
Perfect!
About “Test framework is much more modular.”
How can that be a disadvantage and not an advantage?
Modular systems are simpler, have well-defined responsibilities, and allow for software composition.
I’d love to read a better counterpoint than “a much greater degree of knowledge is required to know what modules are available for a specific page/view/action”
Why is that? You just need to know the modules you will use, and as much as you use them, more you will learn about the system as a whole, which goes hand-in-hand with “It necessitates a thorough understanding of the application's user flows and may be more difficult for inexperienced testers to master”.
Modular systems help less experienced professionals by being specific, small, and easy to understand.
Yes, it's like paradox in context you showed up. I agree. I think mostly I was looking at Filip's and my thoughts from perspective of beginner (as I think his article is mostly for). I've seen that for beginners modules are a little bit more "complicated". Usually I see projects where people throw Cypress Commands around. Maintaining right folder/file structure can be nightmare for them.
But from mid/senior standpoint I totally agree.
I'll try to edit this, to match this comment.Added edit.Thanks for the reply.