Write And Run End-To-End Test Flows With Cypress.io

Cypress makes the process of writing e2es relatively simple with lots of built-in methods and documentation to show you the way to use it effectively.

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the The newline Guide to Modernizing an Enterprise React App course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to The newline Guide to Modernizing an Enterprise React App, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course The newline Guide to Modernizing an Enterprise React App
  • [00:00 - 00:13] In the last lesson, we prepared hardware handler to use Cypress as its end-to- end testing framework, and installed a few extra helper libraries to ensure that the tests we write are quality. Now it's time to start writing those tests.

    [00:14 - 00:42] In this lesson, we'll walk through adding a few end-to-end tests, getting into progressively more complex test setup and actions as we go. Cypress describes itself as a testing framework made to run during active development, and since that is its primary goal, it makes it easy to do some minor configurations, like setting a base URL property that automatically prefixes common Cypress commands, like "side.visit" and "side.request" with this URL.

    [00:43 - 00:55] The first thing that we're going to do is open up our Cypress.json file that was created at the root of our client folder when we first installed Cypress into the project. It should be empty right now, but we're going to go ahead and add the following property to it.

    [00:56 - 01:17] We're going to give it a base URL and tell it that "HTTP colon/localhost3000" is the root of our project. This simple line will prevent us from having to type "localhost3000" over and over again as we add end-to-end tests that navigate around to different pages within the app.

    [01:18 - 01:35] Cool. With that little bit of setup done, let's write our first test. The first end-to-end that I would like to add, one that requires less preceding of data and more generically tests our app's functionality, is to run the app and navigate to each page via the nav bar links and check that they load.

    [01:36 - 01:52] So let's make a new file inside of the Cypress folders integration folder, and we are going to name it "Navigation_spec.js". So new file, navigation_spec.js.

    [01:53 - 02:06] This is where we'll write our first test using the very same describe-and-it tests and text that we're already used to from integration testing. Be aware that for Cypress to be able to run end-to-end tests locally, our app should already be running in a separate terminal window.

    [02:07 - 02:21] Trying to start the server from a Cypress test is considered an anti-pattern, and you can read more about the reasoning behind this decision by Cypress in a link that I provided in the lesson. But the bottom line is just to start the app locally before starting your E2D tests up.

    [02:22 - 02:31] So our first test isn't going to be a fancy one. It's also not going to be very complicated because we want to get a feel for how Cypress works and expects things to be structured.

    [02:32 - 02:42] Let's just make a test to ensure that after our app loads, all of its links in the nav bar successfully move us between different pages in a hardware handler and that those pages load. It should be simple enough.

    [02:43 - 03:16] So inside of our navigation spec.js, let's make a describe block and in its statement to test this functionality. So we are going to describe navigating around the site, and we will wrap this in a function, and we will say it should successfully visit all the pages linked to in the nav bar.

    [03:17 - 03:30] So the first thing that any of our tests will need to do is visit our locally running app, and from there we can start clicking the nav links. So add two lines to our test to visit our app's home page, and we'll check that that page loads and that we can see the page title.

    [03:31 - 04:03] So the first thing that we will do is, scy.visit, this will visit the root of our test, and once we are there we will do a scy.get, and we will search for the h1 in the DOM, and we will say that it should contain "Welcome to Hardware Handler." Cypress's testing syntax feels pretty intuitive, to me anyway.

    [04:04 - 04:19] scy.visit brings us to a particular URL in the app, and scy.get lets us target particular elements on the page. You can do it by HTML tag, element ID or class, data test attributes, and more, which we can then use in a variety of ways.

    [04:20 - 04:30] In this first case, we'll check the text that's present in the DOM. After we've confirmed that the home page is loaded, let's start targeting and clicking each nav link and making sure that each page loads.

    [04:31 - 04:45] To do this, we'll use scy.get to target each text link within the nav bar and click it, and then we'll check that the h1 element that's visible is the right one for that page. So here's how I'd write this test to check that the My Product page loads.

    [04:46 - 05:11] Right under our initial statement, we will do a scy.get, and I am going to look for the nav bar links wrapper class because that's where all of my links are contained, and I'm going to say within that wrapper class, go find the scy.contains of My Products. So go find that particular link and then click it.

    [05:12 - 05:34] And then once that has happened, we'll be able to do another scy.get for the h1 on the new page, and we will say that it should contain my products as the new title. After this code takes us to the list of products page, we'll repeat the same process to check each of our other pages.

    [05:35 - 06:06] So here's how the rest of that test would look, checking the Add New Products and Checkout Pages. So I'm just going to copy this whole thing and paste it twice more, and then switch this from My Products to Add a New Product, and then pop that into the contains, and then switch this one to Checkout, and then check that the page name is also Checkout.

    [06:07 - 06:12] And that's about all I want to do with this first test case. Now's the time to give our one test a try.

    [06:13 - 06:32] So start up your app locally in a terminal window if it's not already running, and the second terminal window, which we will open right here, CD into client, we will do a Yarn Cypress Run. With any luck, the test to navigate around the site to various pages should pass.

    [06:33 - 06:44] And it looks like Add a New Product did not work because this is not Add a New Product on the link to the page. This is actually Add New Products.

    [06:45 - 06:53] That's my mistake. So if we save this and run our Cypress tests again, they passed.

    [06:54 - 07:01] Excellent. After writing our first successful e-to-e, you might notice that there is a bit of duplicate code here.

    [07:02 - 07:12] The duplication I notice is how we're targeting each link in the nav bar. This is a good opportunity to turn this bit of code into a reusable command that any Cypress test will be able to utilize.

    [07:13 - 07:29] To do this, let's head over to our Cypress/support folder and create a new commands folder inside of it. Inside of this new folder, we're going to make a new file that we will name navigation_commands.js.

    [07:30 - 07:46] In this file, we'll take the code that we were using to target each nav link and make it into a reusable function in which we passed the link text to tell it which link to click. So here's one way that we can do that.

    [07:47 - 08:33] We will create a new exported const because we need to be able to access this function and we will call it go to page from nav bar and it will take in a link argument and then it will be a plain arrow function. And we will say, "cy.get" and then we will use the nav bar links wrapper class to target it.within and we will use "cy.contains" and then "backticks $ curly braces" link, which is string templating in ES6.

    [08:34 - 08:43] click. And that is not get random values crypto. Stop trying to overwrite me.

    [08:44 - 08:59] So "cy.get" nav bar links wrapper.within "cy.contains" link.click. To make this function accessible in the whole testing suite, we also need to add this new files exports to the commands.js file in the support folder.

    [09:00 - 09:17] Inside of commands.js, it already has some sample commands that are commented out, but I'm just going to leave them for future reference and work around them. So the first thing that we need to do is import any and all navigation commands that we've defined inside of our navigation commands.js file.

    [09:18 - 09:40] So on line 10, I am just going to uncomment that and do an import star as navigation commands from, and then the relative path to commands navigation commands. Then give that individual command a name that Cypress tests can use to reference it.

    [09:41 - 10:00] Names similar to the function name itself are the easiest to remember. So right down here, we will do Cypress.man's.add, and we will call this go to "page from nav bar", and then we will do "navigation commands.go to page from nav bar".

    [10:01 - 10:16] With this completed, any Cypress test should be able to use this new custom command. Back in our navigation spec.js, we can replace each line targeting the nav bar links with our new "side.go to page from nav bar" command.

    [10:17 - 10:23] There are no extra imports required in the file to access this command. It works just like any other built-in Cypress method.

    [10:24 - 10:44] So here is what our first test will look like once we have made the changes. We will get rid of all of this "side.get" nav bar links wrapper, and we will replace it with "side.go to page from nav bar".

    [10:45 - 10:57] I need to correctly capitalize that "b", and then we will just pass in the same string of my products. So if we copy that, we will replace these.

    [10:58 - 11:07] My products will become "add new products". My products will become checkout.

    [11:08 - 11:11] Look how much cleaner that is. So much easier to read, right?

    [11:12 - 11:22] And it's handy to know that any Cypress test file will be able to use this new method going forward. Just to make sure things still run, give our e2e tests another run, and then we will move on and write another one.

    [11:23 - 11:28] So once more, do a "yarn Cypress run". Very good.

    [11:29 - 11:37] Another flow that I'd consider critical to this application is the ability for users to add new products. So let's write a test that checks that this flow works correctly.

    [11:38 - 11:52] This is going to require some initial data seating as well because when a user navigates to the "add new products" page, the department API is called to get all the departments to display in the form 's department drop-down. Ready to set up some mock departments?

    [11:53 - 12:05] So before we mock the API call to return departments, we need to make that department data. Inside of our fixtures folder, we are going to make a new file called "dep artment_data.json".

    [12:06 - 12:17] Department_data.json, and this is where our mocked department's data will end up living. Just like with real department data, we'll return a list of departments with ID and name properties.

    [12:18 - 12:30] And since this app currently has no validations around the departments, I'll add two new departments that will only be present in our e2e's, flooring and patio furniture. Feel free to add whatever you'd like, but that's what I'm going to do.

    [12:31 - 12:44] So here we are going to do departments, and it takes in an array of objects. So we have an ID, which I will do ID of 37 and name of flooring.

    [12:45 - 13:04] And then our second one is going to be, oh, that needs to be string as well. And then our second department is going to have an ID of 22 and a name of patio furniture.

    [13:05 - 13:11] Right. Now with some mock department data, we can set up a mocked call to the department API to return this data.

    [13:12 - 13:24] So open up our support folder and make a new folder alongside the commands one, but call this "service_mocks". So we have a new folder and we are going to call it service_mocks.

    [13:25 - 13:35] And in this folder, we are going to create a file named department_mocks.js. So new file, department_mocks.js.

    [13:36 - 13:43] And this is where all the mocks for our API calls will live. Mocked API calls in Cypress tend to follow a similar pattern.

    [13:44 - 13:53] Use the "side.route" method to define the route, and then inside of the method, declare if it's a "get", a "post", etc. Define the URL that this mock should be matched to.

    [13:54 - 14:05] Define a response that the function should return and an HTTP status, and then give the new "side.route" object an alias to reference it. I tend to name it the same as the function name, but you do whatever you feel like.

    [14:06 - 14:26] The response and the status are optional, but if the function should return something after the mock is called, this is one way to do it. Likewise, if the real API call takes in an argument, like info from a form, submission, or query params from the URL, the additional function arguments get passed into the function before the arguments defining the response and the status.

    [14:27 - 14:36] If it's still a little unclear, it should make more sense when we go through the code. So for our Get All Departments API call, here is what the code for that mock will look like.

    [14:37 - 14:57] Just reference what the real Get All Departments call is doing to inform you of how to construct this mock. So we are going to do an export, const, getAll Departments, and then here we will have our response argument and our status, which we will default to equ aling 200.

    [14:58 - 15:11] So in here we will have psi dot route and an object where our method will be a get. Our URL will be slash departments.

    [15:12 - 15:28] Our response will be whatever argument we pass in, status will default or take whatever we pass for that. And then we are going to name this as Get All Departments.

    [15:29 - 15:49] Since Get All Departments is a get HTTP call, the only two arguments that can be passed to the mock function are the response from this call, the mock department data that we just defined, and status which will default to 200 when not defined as otherwise. The URL property is pretty self explanatory and will pass the response and the status into the function ourselves.

    [15:50 - 15:56] So let's get to the actual test writing part and see how this all comes together. Here's a little tip though that I'd like to share with you.

    [15:57 - 16:06] Make mock files that match your real service API files. For mocking API calls, I prefer to make separate mock files to match the actual API files.

    [16:07 - 16:22] In our project which has three API files, department API, product API, and checkout API, I'd make three mock files to hold the different API call mocks. This is a simple way to keep your API calls organized within your Cypress tests .

    [16:23 - 16:33] Okay, time to make this test. So we're going to create a new test file for this E2E as it's concerned with different user interactions than our navigation spec.js.

    [16:34 - 16:53] So back in our integration folder, create a new file named something like add new product spec.js. So add underscore new underscore product underscore spec.js to denote what these tests are going to be all about.

    [16:54 - 17:09] And just as before, we'll make a describe block to begin our test suite. Unlike our previous E2E though, we'll need to add a before each code block to this test because in order for a user to be able to add a product, they're required to choose a department from the drop down of potential departments.

    [17:10 - 17:24] So here's how we're going to set this up. We're going to do an import star as department mocks from and then our relative path to support slash service mocks slash department mocks.

    [17:25 - 17:34] And then we're going to create our describe block. And we will say add a new product to hardware handler.

    [17:35 - 17:40] And then we will make that into an arrow function. And we will set up our before each.

    [17:41 - 17:50] Very similar to the way that we do integration testing set up right now. And we will say, sigh.visit.

    [17:51 - 17:57] And we will head to our homepage first because that's the entrance. And then we will do side.server.

    [17:58 - 18:05] And I will explain that shortly and we will do side. fixture. And we will access our department.

    [18:06 - 18:15] Department underscore data.json file. And in a dot then we will have a response which we'll just call RC.

    [18:16 - 18:29] And we will say department mocks dot get all departments is going to return RC dot departments. Finally, we will have side dot go to page from nav bar.

    [18:30 - 18:37] And we will head to the add new products page. And then we will do a side dot wait.

    [18:38 - 18:46] On at symbol get all departments. Let's talk about what's happening in this before each.

    [18:47 - 18:57] Right after our site is reached via the side.visit method, side.server is called to start a server and begin routing responses to side. route to change the behavior of the network requests.

    [18:58 - 19:14] Side.server is required for our mocked API calls to take effect. After that we use the side. fixture method to bring in the mocked department data.json file that we defined and reference its departments array to be returned in the callback when this function is called.

    [19:15 - 19:26] Make a note that we only have to import the mocked API columns file. The mocked data in our department underscore data dot.json file is directly referenced within the side. fixture method.

    [19:27 - 19:33] Now that function is ready to be intercepted as soon as it's called. And it will return our mocked department data instead of the real department data.

    [19:34 - 19:49] At this point we'll use our new custom command side dot go to page from nav bar to get to the add new products page. Finally, the side.weight method is passed the at get all departments alias that we defined in our department mocks.js file.

    [19:50 - 20:03] This function makes the cypress test wait for the resource to be ready before moving on. It's probably unnecessary for the small amount of data that we're passing, but if we're dealing with bigger, more complex objects waiting for data might be required.

    [20:04 - 20:18] And here's a quick way to tell if an XHR request is real or mocked. If you're not sure if one of your HTTP requests that you're attempting to stub is successfully being mocked or not, you can open the cypress test runner and watch the test run.

    [20:19 - 20:34] A successfully mocked request will have a tiny XHR stub badge next to it when it's been intercepted and replaced with mocked data. Whereas an HTTP request is not mocked, we'll just have XHR next to it in the test steps, just so you know.

    [20:35 - 20:49] So with our preceded data in place, we can now make our test that will fill in each input in our add a new product form and then submit the data. So we'll begin by describing what we're actually testing, filling out a form and successfully submitting it.

    [20:50 - 21:26] So our first hit will say it should successfully accept a new product into the system when a user fills in all the forms, fills in all the form fields. And then we will make our arrow function. Then if we look at how the actual product API works that gets called when a new product is submitted, you'll notice that it's a product object with a bunch of properties that match most of the inputs we'll need to fill in.

    [21:27 - 21:42] So let's create an object that we can pass when we mock the API call and use it to fill in our form. I declared a variable named new tool info to contain our mocked data. So let's do that. We will have a const new tool info.

    [21:43 - 22:16] And we will give it a department ID of 22, a name of antilever umbrella, a brand of that gate, a description. Of the fanciest umbrella with an ultra strong curved arm. So there is no poll.

    [22:17 - 22:30] Marring your view. And finally, a retail price of 2900. Okay, so we need to mock our product API call of add new product before we fill out the form in our test.

    [22:31 - 22:43] So head back over to our service mocks folder and create a new file named product underscore mocks.js. New file product underscore mocks.js.

    [22:44 - 22:55] This mock for add new product will look similar to the other mock that we made forget all departments. It still takes in a response and a status and will define a method, a URL and an alias by the same name as our function.

    [22:56 - 23:21] Contrary to our prior API call, this one is a post and it will accept a request body, the new product info argument, although it won't actually do anything with it. So we will have an export const add new product function and it will take in new product info, a response, and a status that we will default to 201.

    [23:22 - 23:42] And then in here we will have our psi dot route. This object will be a method of host, a URL of products, a response, a status.

    [23:43 - 23:54] And finally we will give it an alias of add new product. With our mock in place we can import it into our test file and pass the new tool info object that we defined earlier.

    [23:55 - 24:09] We'll also need to import the add new product success constant because that is our response argument from this API. So if we head back to our add new product spec, we are going to add a couple of imports at the top.

    [24:10 - 24:37] So we are going to import the add_new_product_success constant from our relative path to our constants, which is going to be source constants constant. And we are going to also import star as product_mocks from our support folder.

    [24:38 - 24:48] Support service_mocks product_mocks. And below we will define the mock inside of our test right after the new tool info is declared.

    [24:49 - 25:09] This is definitely a call that we want to mock because otherwise if we were running our E2Es multiple times or we had many devs on the team all running them for their development branches , our local database of products could end up with hundreds or possibly thousands of the same test umbrella. So by intercepting the actual form request for the mock, we can avoid that situation.

    [25:10 - 25:26] So our new product mock will be product_mocks.add_new_product_info. We will pass it our new tool info and we will give it add_new_product_success and a 201 as the response and the status that we wanted to return.

    [25:27 - 25:36] And at this point we can begin to actually fill in our test form. So to pick one of our mock departments in the drop-downs, we are going to use the "Sci.Select" method.

    [25:37 - 25:49] To fill in each of the inputs requiring a text, we will use the "Sci.Clear" method to clear any previous text in the input just as a precaution. Then we will chain that to "Sci.Type" which will actually fill in the input.

    [25:50 - 25:58] And we will be able to use our new tool info object to provide most of the input values. After all the inputs are filled in, we will click the button to add the product .

    [25:59 - 26:11] So we will do a "Sci.Get" and the first thing is the department ID. And we will do a .select and pass it "Paddio Furniture".

    [26:12 - 26:17] So that's from our drop-down. Then we will do a "Sci.Get" of name.

    [26:18 - 26:31] We will clear it to make sure that there is nothing already in the input and then we will type "newtoolinfo.name". We will do the same thing for "Sci.Get" brand.

    [26:32 - 26:54] Clear and then type "newtoolinfo.brand.Sci.Get" for description. Clear and then type "newtoolinfo.description.Sci.Get.price".

    [26:55 - 27:11] Clear that and then type "newtoolinfo.retailprice". And finally we will get the button to actually submit this and do a .click on that.

    [27:12 - 27:27] So when this button is clicked and the post requests add a new product fires, our mock will take over and return us the "add new product success" constant as the API response. We can then check that this response triggers our React Toastify success message in the browser.

    [27:28 - 27:52] So right after we've done that click we will do a "Sci.Get" and we are going to look for the "Toastify_Toast-Body" which is the class on our toasts and we will say .should contain "add new product success". And that should be another "EtoE" test done.

    [27:53 - 28:04] So this is the moment of truth, run all of our "EtoE's" and see how they do. With any luck your test run printed out to the terminal should be successful.

    [28:05 - 28:17] Success. Congrats! That's how to write end-to-end tests with Cypress. It takes a little getting used to but the syntax is easy to pick up, especially if you've used something like jQuery in the past.

    [28:18 - 28:34] There are lots of handy methods and the documentation really is well done and up to date. Our next lesson will focus on Cypress Studio, an experimental feature that lets us generate tests within the Cypress test runner by recording interactions that we make against the application.

    [28:35 - 28:41] Yes, you heard that right. You interact with the app and Cypress writes tests mimicking your interactions.

    [28:42 - 28:43] Have I peeped your interest?