This is Part 2 of a series of articles I’m writing on automated testing, (specifically React testing), and the value the JDLT team get from it.
In Part 1, React component testing with Jest and React Testing Library, I explored how I use automated tests as a way of adding value in my first role as a developer in a company with a large code-base to learn.
Back then, I focussed on Unit testing and Integration testing while using Jest and React Testing Library. This time I’ll be writing about End-to-End testing and how we use it to give us confidence our apps work as intended.
So, what are Unit, Integration and End-to-End tests? Well, most automated tests fall into one of three types:
Type of Test | What does it test? It tests … | Popular tools |
---|---|---|
Unit | Small independent parts work as expected. Isolated function/classes/algorithm. E.g. Shared functions usually found in the utils folder. | Jest and React Testing Library / Enzyme |
Integration | Several units/parts work together as expected. E.g. Two React components where one is the parent while passing props to its child. | Jest and React Testing Library / Enzyme |
End-to-End | Behaves as the user would, clicking/typing around the app. Runs an entire app in the browser and interacts with both front and back end as a user would. Like a robot user! | Cypress or Puppeteer and Jest |
I'll assume you have a basic knowledge of TypeScript and React. I use a MacBook for development so I'll be using Zsh & Brew and I also use Yarn rather than NPM. I will be using a React project I worked on previously to show some examples.
Cypress and Jest & Puppeteer seem to get a considerable amount of attention for their accessibility and cost effectiveness when it comes to End-to-End testing. I will therefore produce two articles exploring my experience using these tools with a React app.
This article will focus on Cypress and Part 3 will focus on Jest and Puppeteer.
Cypress is a tool developed by a company of the same name. The test runner is open source and free although they offer paid services that integrate with it.
Cypress has been designed to be the End-to-End testing tool of choice and as result it is very reliable and makes testing enjoyable for the developer. It supports testing in different browsers, and it is very easy to get started because there are no servers, drivers, or dependencies to install or configure. Cypress also offers out of the box examples of tests and video recordings of each session which you can leverage.
Install Cypress:
$ npm install cypress --save-dev
Or
$ yarn add -D cypress
Adding Cypress commands to scripts in package.json
:
...
"scripts": {
"cypress-open": "cypress open",
"cypress-run": "cypress run --browser chrome"
}
...
cypress run –browser chrome
will default to chrome as a browser when running tests.
Cypress-open
will open the cypress console window while Cypress-run
just runs tests and when all the tests have run, it shuts down cypress and the browser.
While configuring cypress bear in mind to include baseUrl
to tsconfig.json
and make the relevant changes to cypress.json
should you decide to move the cypress folder from root or define options for when tests are ran (check Cypress configuration docs for more information).
Example of cypress.json
should you decide to move the cypress folder to a folder called tests which lives in root folder of the project:
{
"cypressFolder": "test/cypress",
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/integration",
"pluginsFile": "test/cypress/plugins/index.js",
"screenshotsFolder": "test/cypress/screenshots",
"videosFolder": "test/cypress/videos",
"supportFile": "test/cypress/support/index.js"
}
To ensure your tests don’t break inadvertently, use of the tag data-cy
as selector identifiers instead of implementation details such as CSS classes or DOM location. Doing so will improve accuracy in targeting the correct element and it will make your tests more resilient to future changes to the component’s implementation.
What to test in a simple To Do list
app?
When thinking of your End to End
tests be clear of the user functions of what you’re testing (e.g. login into an user area) and build conditions (e.g. invalid vs valid username and password).
Imagine we have an app to help us manage our To DO list
and that we want to test it. Our app is very simple therefore our tests will cover:
For each scenario there will be a series of different test because the build conditions may change, for example what if the input field is empty or if the name is the same as an existing ‘to do’ item.
Render without crashing:
User functions:
Build conditions:
To Do items
,To Do items
,To Do items
.Add a new ‘to do’ item to the list:
User functions:
Build conditions:
To Do item
from being created,To Do item
,To Do list
to To Do Item
as props, each To Do Item
should render the text that was passed down to it.Delete an existing ‘to do’ item:
User functions:
To Do item
, remove that To Do item
from the App.Build conditions:
To Do item
has been removed from the app, the second item should now become the first (and only) item.Cypress will create many examples of tests within the integration
folder, use it to get more familiar with the syntax and how to structure your tests – delete these when and if not needed. I will therefore add a very simple and straight forward example of a Cypress test. I also recommend you use Cypress docs to get familiar with the extensive number of assertions, commands and utilities.
The naming convention for testing files is to end it on *_spec.ts
, as per our test below within the file my-test_spec.ts
. The tests should be placed in the integration
folder.
describe("<App/>", () => {
before(() => cy.visit("http://localhost:3000/"))
it("Renders without crashing", () => {
cy.get("span").contains("My Todo List")
})
describe("The default Ui", () => {
it("Renders two default todo items", () => {
cy.get(".row").should("have.length", 2)
})
it("Has an input field", () => {
cy.get("input").should("have.length", 1)
})
it("Has an Add button", () => {
cy.get('[data-cy="addButton').should("have.text", "Add")
})
})
})
;<button
data-cy="addButton"
type="submit"
className="btn btn-primary mb-2 col-4 col-sm-2"
>
Add
</button>
All tests use describe()
blocks and it()
functions, a similarity you will notice to other tests you may have used in the past. The pattern to follow would be a describe()
block with it()
statements inside it.
The it()
function takes a string which describes what is being tested and a call-back function. The call-back will in general check/find something (cy.get('[data-cy="addButton') the add button in this case and then check an assertion against it (.should("have.text", "Add").
Cypress was built with flaky
tests (pass or fails periodically without any code changes) in mind but don’t forget to change assertion’s condition to make tests fail as well as pass.
End-to-End testing should focus on replicating user behaviour and interact with both the front and back ends as a user would do. Broad tests which cover user function and build conditions are better than implementation details as these are less likely to break.
At JDLT, we create custom business software for SMEs and large organisations.
Please use our contact us page or get in touch at hi@jdlt.co.uk if you'd like to talk about how we can help.