Write once and run multiple test cases with Cypress
Very early in my test automation career, I came across the legendary software engineer and author Robert C. Martin (aka Uncle Bob) who taught me that unnecessarily repeating code was a terrible idea. Not only because it leads to software systems that are hard to maintain and more prone to bugs and errors. But if large parts of the code base is compromised of code that is copy + pasted with a few variable names changed. Then if volatility is introduced via a change in the requirements or modification to implemented logic, then large parts of the solution will be made redundant with the click of someone else’s mouse.
So for combating this, instead of writing code to perform multiples of the same function with only a few slight changes to the data we are using. We should instead focus on only coding one test (in our case) and rely instead on the test automation framework to run our required test cases with the various scenarios that we ask of it. The benefits of this go beyond a solution that is easier to maintain and more flexible to outside changes. It also enables us to ramp up our testing efforts and run more test cases if needed.
For instance, you might only need to run 5 tests at the moment. But what if you need to run 15, or even 50 one day? With a code once and test multiple times approach. The ability for us to scale is much easier than if we took the approach of repeating every test case and making a few alterations.
This approach is not applicable in every scenario, and there may be situations in which we implement logic in one of our test cases and needs to be used in another. But this alone should not be a reason to take the simple approach and reuse code. It instead should only be done if there is no other solution available.
Defining our problem
Before we start to code, we should start with a basic understanding of what we are trying to achieve.
In essence, our goal is to have a single test case that is reusable and allows us to pass in our predefined test data and be able to run it with no modifications to the test code.
We will use the example of a fictional web application in which our tests are based on the prerequisite of logging into the application to perform our desired testing functions. In this example we simply want to assert that the page we are navigating to once we are logged in is displaying the correct page title. Instead of putting that login code in every it
block and repeating ourselves. Let’s instead place it in with the provided beforeEach
block so it is run automatically before the execution of each of our test cases.
describe(`Dynamic Data Test`, () => { beforeEach(() => { cy.visit("/login"); cy.get('#username').type('admin') cy.get('#password').type('password') cy.contains("button", "Login").click() }); });
Personally, I would use the Custom Commands feature the Cypress provides to abstract away this logic and make my Cypress code more descriptive. You can read about Custom Commands here.
describe(`Dynamic Data Test`, () => { beforeEach(() => { cy.visit('/login') cy.login({ username: 'username', password: 'password' }); }); it("Makes sure the homepage is visible", function () { cy.contains('Welcome to the homepage!').should("be.visible") }); });
That’s better.
This test layout is fine if we want to use a fixture to supply our test data, but for our needs, I find it is better to write a require statement to load and store our data:
const testData = require("../fixtures/test_data/data.json") describe(`Dynamic Data Test`, () => { beforeEach(() => { cy.visit('/login') cy.login({ username: 'username', password: 'password' }); }); it("Makes sure the homepage is visible", function () { cy.visit('/home') cy.contains('Welcome to the homepage!').should("be.visible") }); });
We’re making progress! But our test is still static at the moment. We still have the problem of having to write multiple tests to check the title of various pages. So let’s add some more code to make it run our dynamic test cases.
Using JavaScript’s built-in looping
The first thing to realise is that out of the box, JavaScript is already fantastic at running the same bits of code repeatedly. We only need to tell it what we want it to do and if will repeat the designated code for us. But what do we want to repeat?
In the code above, we are loading a JSON file and assigning it to the variable testData
. So let’s start by making sure that we have the contents of the file set up how we want it so that JavaScript can work with it.
[ { "Title": "Homepage", "location": "/home" }, { "Title": "Service Page", "location": "/services" }, { "Title": "Contact Page", "location": "/contact" }, ]
Here we are basically using an array of JSON objects with the first pair of key and values representing the title we expect, and the second being the location of where we are expecting to see it. Of course, this is a very simplistic demo. But you can extend it to be more complex.
The final step is to put it all together:
const testData = require("../fixtures/test_data/data.json") testData.forEach((testCase) => { describe(Dynamic Data Test, () => { beforeEach(() => {
cy.visit('/login') cy.login({ username: 'username', password: 'password' }); }); it(Makes sure the ${testCase.title}} is visible, function () { cy.visit(${testCase.Location}) cy.contains(`Welcome to the
${testCase.Title}`).should("be.visible") }); }); });
This code will use the data stored within the JSON file to drive our defined test case without the need to repeat ourselves with multiple spec files. Testing within different scenarios is something that every test automation solution needs to do, but we needn’t do it at the detriment to the maintainability and future scalability of our framework. So next time before you write a bit of code to test a piece of functionality, ask yourself. Is there already a test that you could extend or use here?
I hope you found this blog post useful, Feel free to connect with me on Linkedin, Twitter, or via my contact form if you have a test automation question. Or would just like to say hello.
Hi Kevin, Thanks for the details on the blog,
I am having a question in the following code I was trying to iterate my Testcase for a set of test data which would be loaded from the excel file on runtime inside the before hook. Here my issues is I am not able to get the desired result as testData.forEach shows undefined i.e the scope of the testData I got from beforehook is not extended to forEach. Any suggestion on this to get it working ?
===================================================================
import excelLoader from ‘../TestData/XLSXtoJSON’
describe(‘ExcelReader’,function ()
{
before(function(){
cy.task(‘excelReader’) // this will load my excel file to testData.Json file –function inside plugin index.js file
cy.fixture(‘testData’).then(function (data){
this.testData=data;
cy.log(this.data)
});
});
testData.forEach(function(testDataRow) {
const data = {
password: testDataRow.password,
userName: testDataRow.userName
}
it(‘Login’,function(){
//my test case here
})
})