CodeNewbie Community

Cover image for How to do Test Driven Development in React using React Testing Library, Jest and Cypress
Andrew Baisden
Andrew Baisden

Posted on

How to do Test Driven Development in React using React Testing Library, Jest and Cypress

What is Test Driven Development?

Test Driven Development or TDD for short is essentially a process that developers and teams go through when they are testing their code. Coding, design and testing are combined together and test cases are created to ensure that the code has been robustly tested and any bugs or errors have been resolved in the development phase prior to it reaching production level.

This is considered to be good practice and a methodology that all developers should be following when they are working on a codebase. Through this process the code improves over time leading to a much more stable application. In this article we will look at Unit Tests, Integration Tests and End-To-End Tests.

What are Unit Tests?

Basically a Unit Test is a method of testing small samples of code within an application. This can include functions that run code blocks or API's which return data. The goal is to find out if the code is working properly and if it is catching any errors when they occur. Like for example incorrect data getting returned in a form.

What are Integration Tests?

Integration Tests are pretty much just multiple Unit Tests grouped together. So whereas a single Unit Test would test one piece of functionality an Integration Test is more like a test suite. So in a sense you are now testing multiple code blocks at the same time like for example an entire carousel component. If it was a Unit Test then you would only be testing to see if an image was loading whereas in an Integration Test you are now testing to see if the title is loading, image is loading, and the correct data is showing up etc... Integration tests are great for testing user flows.

What are End-To-End Tests?

End-End Tests are a way to test an applications frontend workflow. It is a method to test the whole application so that you know it's going to behave the way you expect it to. The difference between End-To-End testing and the other two is that End-To-End testing tests the software and the system whereas the other two are more for systematic testing.

How to do testing?

Jest and React Testing Library are extremely popular when it comes to doing Unit and Integration tests in the command line. Cypress is a popular tool for doing End-To-End testing in the browser. Jest can even be used on the backend so you can cover all your bases and use the same library for backend and frontend testing work.

Unit Test/Integration Test libraries

End-To-End Test libraries

Project Setup

Let's setup our project. Navigate to a directory on your computer open up the command line and run the commands below.

npx create-react-app tdd-react-cypress-app
cd tdd-react-cypress-app
npm install cypress @testing-library/cypress --save-dev
mkdir src/components
mkdir src/components/{Form,Header,Profile,ProfileDetails,Sidebar}
touch src/components/Form/{Form.js,Form.test.js,Form.css}
touch src/components/Header/{Header.js,Header.test.js,Header.css}
touch src/components/Profile/{Profile.js,Profile.test.js,Profile.css}
touch src/components/ProfileDetails/{ProfileDetails.js,ProfileDetails.test.js,ProfileDetails.css}
touch src/components/Sidebar/{Sidebar.js,Sidebar.test.js,Sidebar.css}
Enter fullscreen mode Exit fullscreen mode

Now run this command to start Cypress you should see a Cypress window open on your computer.

# To start Cypress
npx cypress open
Enter fullscreen mode Exit fullscreen mode

There are lots of example integration tests if you want you can run them to see what they do. When you are ready open the project in your code editor, go inside of your project and find the Cypress integration folder at my-app/cypress/integration and delete the folders inside of it so we have a clean slate.

Then create a file called user.spec.js and put it inside of the integration folder with the code below. This will be out first End-To-End test but its not going to work yet because our application has no code!

describe('user form flow', () => {
    beforeEach(() => {
        cy.viewport(1600, 900);
        cy.visit('http://localhost:3000/');
    });

    it('user can save form', () => {
        // save form data

        cy.get('input[name="firstName"]').type('Eren');

        cy.get('input[name="lastName"]').type('Yeager');

        cy.get('input[name="email"]').type('erenyeager@gmail.com');

        cy.get('input[name="career"]').type('Attack Titan');

        cy.get('textarea[name="bio"]').type('Hello there my name is Eren Yeager!');

        cy.get('input[name="save"]').click();
    });
});
Enter fullscreen mode Exit fullscreen mode

It's finally time to add the code to the files we created earlier. Copy and paste the code below into their corresponding files. It's quite a tedious process because they are separated into components but it will be worth it in the end.

Alternatively you can just clone/download the repo and skip to the end of this article which is the Unit Tests and Integration Tests section.

https://github.com/andrewbaisden/tdd-react-cypress-app

App Component Files

App.css

@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap');

*,
*::before,
*::after {
    padding: 0;

    margin: 0;

    box-sizing: 0;
}

html {
    font-size: 16px;
}

body {
    font-family: 'Quicksand', sans-serif;

    font-size: 1.6rem;

    color: #2d2d2d;

    background: #b3b3b3
        url('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2670&q=80');
}

.container {
    margin: 2rem auto;

    display: flex;

    flex-flow: row nowrap;

    width: 100%;

    height: 50rem;

    max-width: 100rem;
}

main {
    width: 100%;

    max-width: 60rem;
}
Enter fullscreen mode Exit fullscreen mode

App.js

import Sidebar from './components/Sidebar/Sidebar';

import Header from './components/Header/Header';

import Profile from './components/Profile/Profile';

import './App.css';

const App = () => {
    return (
        <>
            <div data-testid="container" className="container">
                <Sidebar />

                <main>
                    <Header />

                    <Profile />
                </main>
            </div>
        </>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

App.test.js

import { render, screen } from '@testing-library/react';

import App from './App';

describe('<App />', () => {
    it('has a container div', () => {
        render(<App />);

        const el = screen.getByTestId('container');

        expect(el.className).toBe('container');
    });
});
Enter fullscreen mode Exit fullscreen mode

Form Component Files

Form.css

.profile-details-form-container {
    margin-top: 2rem;

    display: flex;

    flex-flow: column nowrap;
}

.profile-details-form-container input {
    width: 100%;

    height: 2rem;

    padding: 0.5rem;

    font-size: 1.3rem;
}

.profile-details-form-container label {
    width: 100%;
}

.profile-details-form-container textarea {
    width: 100%;

    height: 5rem;

    resize: none;

    padding: 0.5rem;

    font-size: 1.3rem;
}

input[type='submit'] {
    border: none;

    background: #7e7dd6;

    color: #ffffff;

    font-weight: 600;

    width: 8rem;

    border-radius: 0.2rem;

    cursor: pointer;

    font-size: 1rem;
}

.form-output {
    margin-top: 1rem;

    width: 40rem;

    font-weight: 600;

    font-size: 0.8rem;
}
Enter fullscreen mode Exit fullscreen mode

Form.js

import { useState } from 'react';

import './Form.css';



const Form = () => {

const [firstName, setFirstName] = useState('');

const [lastName, setLastName] = useState('');

const [email, setEmail] = useState('');

const [career, setCareer] = useState('');

const [bio, setBio] = useState('');

const [data, setData] = useState('');



const formSubmit = (e) => {

e.preventDefault();



const user = {

firstName: firstName,

lastName: lastName,

email: email,

career: career,

bio: bio,

};



const formData = JSON.stringify(user);



console.log(formData);

setData(formData);

clearForm();

};



const clearForm = () => {

setFirstName('');

setLastName('');

setEmail('');

setCareer('');

setBio('');

};



return (

<>

<div>

<form onSubmit={formSubmit} className="profile-details-form-container">

<div>

<label data-testid="firstname">First Name</label>

<input

type="text"

name="firstName"

value={firstName}

onChange={(e) => setFirstName(e.target.value)}

placeholder="First Name"

/>

</div>

<div>

<label data-testid="lastname">Last Name</label>

<input

type="text"

name="lastName"

value={lastName}

onChange={(e) => setLastName(e.target.value)}

placeholder="Last Name"

/>

</div>

<div>

<label data-testid="email">Email</label>

<input

type="text"

name="email"

value={email}

onChange={(e) => setEmail(e.target.value)}

placeholder="Email"

/>

</div>

<div>

<label data-testid="career">Career</label>

<input

type="text"

name="career"

value={career}

onChange={(e) => setCareer(e.target.value)}

placeholder="Career"

/>

</div>

<div>

<label data-testid="bio">Bio</label>

<textarea name="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Bio"></textarea>

</div>

<div>

<input name="save" type="submit" value="Save" />

</div>

</form>

<div className="form-output">

<p>Output</p>

<div>{data}</div>

</div>

</div>

</>

);

};



export default Form;
Enter fullscreen mode Exit fullscreen mode

Form.test.js

import { render, screen } from '@testing-library/react';

import Form from './Form';



describe('<Form />', () => {

it('has a first name label', () => {

render(<Form />);

const el = screen.getByTestId('firstname');

expect(el.innerHTML).toBe('First Name');

});

it('has a last name label', () => {

render(<Form />);

const el = screen.getByTestId('lastname');

expect(el.innerHTML).toBe('Last Name');

});

it('has a email label', () => {

render(<Form />);

const el = screen.getByTestId('email');

expect(el.innerHTML).toBe('Email');

});

it('has a career label', () => {

render(<Form />);

const el = screen.getByTestId('career');

expect(el.innerHTML).toBe('Career');

});

it('has a bio label', () => {

render(<Form />);

const el = screen.getByTestId('bio');

expect(el.innerHTML).toBe('Bio');

});

});
Enter fullscreen mode Exit fullscreen mode

Header.css

header {
    background: #ffffff;

    display: flex;

    flex-flow: row nowrap;

    justify-content: space-between;

    padding: 1rem;

    border-bottom: 0.1rem solid rgb(234, 234, 234);
}

.page-title,
.page-info {
    display: flex;

    flex-flow: row nowrap;

    justify-content: center;

    align-items: center;
}

.page-title h1 {
    font-size: 2rem;
}

.page-info {
    display: flex;

    flex-flow: row nowrap;

    justify-content: space-around;

    max-width: 15rem;

    width: 100%;
}

.page-info button {
    border: none;

    background: #7e7dd6;

    color: #ffffff;

    padding: 1rem;

    border-radius: 0.5rem;

    font-weight: 600;

    cursor: pointer;
}

.secure,
.notifications {
    display: flex;

    flex-flow: row nowrap;

    justify-content: center;

    align-items: center;

    border: 0.2rem solid rgb(233, 233, 233);

    padding: 0.5rem;

    height: 2rem;

    border-radius: 0.5rem;
}
Enter fullscreen mode Exit fullscreen mode

Header.js

import './Header.css';

const Header = () => {
    return (
        <>
            <header>
                <div className="page-title">
                    <h1 data-testid="info">Information</h1>

                    <div>πŸ“</div>
                </div>

                <div className="page-info">
                    <div className="secure">πŸ›‘</div>

                    <div role="alert" className="notifications">
                        πŸ””
                    </div>

                    <button data-testid="confirm-btn">Confirm</button>
                </div>
            </header>
        </>
    );
};

export default Header;
Enter fullscreen mode Exit fullscreen mode

Header.test.js

import { screen, render } from '@testing-library/react';

import Header from './Header';

describe('<Header />', () => {
    it('has a title h1', () => {
        render(<Header />);

        const el = screen.getByTestId('info');

        expect(el.innerHTML).toBe('Information');
    });

    it('has a notification div', () => {
        render(<Header />);

        const el = screen.getByRole('alert');
    });

    it('has a confirm button', () => {
        render(<Header />);

        const el = screen.getByTestId('confirm-btn');

        expect(el.innerHTML).toBe('Confirm');
    });
});
Enter fullscreen mode Exit fullscreen mode

Profile.css

.profile-container {
    display: flex;

    flex-flow: row nowrap;

    justify-content: space-between;

    padding: 1rem;

    background: #ffffff;
}

.profile-container section {
    margin: 1rem;
}

.profile-container h1 {
    font-size: 1.5rem;
}

.profile-container p {
    font-size: 1.3rem;
}
Enter fullscreen mode Exit fullscreen mode

Profile.js

import Form from '../Form/Form';

import ProfileDetails from '../ProfileDetails/ProfileDetails';

import './Profile.css';

const Profile = () => {
    return (
        <>
            <div className="profile-container">
                <section>
                    <article>
                        <h1 data-testid="user-profile">User Profile</h1>

                        <p>Fill in your user details in the form below.</p>
                    </article>

                    <Form />
                </section>

                <section>
                    <ProfileDetails />
                </section>
            </div>
        </>
    );
};

export default Profile;
Enter fullscreen mode Exit fullscreen mode

Profile.test.js

import { screen, render } from '@testing-library/react';

import Profile from './Profile';

describe('<Profile />', () => {
    it('has a heading', () => {
        render(<Profile />);

        const el = screen.getByText(/User Profile/i);

        expect(el).toBeTruthy();
    });
});
Enter fullscreen mode Exit fullscreen mode

ProfileDetails.css

.profile-details-container {
    width: 20rem;
}

.profile-details-container p {
    font-size: 1rem;

    font-weight: 600;

    margin-top: 1rem;
}

.profile-details-container form label {
    font-size: 1rem;

    margin-left: 1rem;
}

.profile-details-image {
    display: flex;

    flex-flow: column nowrap;

    align-items: flex-start;
}

.profile-details-image h1 {
    font-size: 1.2rem;

    margin-bottom: 1rem;
}

.profile-details-image div {
    background: #7e7dd6;

    border-radius: 100%;

    height: 5rem;

    width: 5rem;

    display: flex;

    flex-flow: row nowrap;

    justify-content: center;

    align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

ProfileDetails.js

import './ProfileDetails.css';

const ProfileDetails = () => {
    return (
        <>
            <div className="profile-details-container">
                <div className="profile-details-image">
                    <h1>Profile Photo</h1>

                    <div>😎</div>
                </div>

                <p>Select your gender</p>

                <form>
                    <div>
                        <input type="radio" id="male" name="male" value="Male" />

                        <label htmlFor="male">Male</label>

                        <br />
                    </div>

                    <div>
                        <input type="radio" id="male" name="male" value="Male" />

                        <label htmlFor="female">Female</label>

                        <br />
                    </div>

                    <div>
                        <input type="radio" id="male" name="male" value="Male" />

                        <label htmlFor="nonBinary">Non-binary</label>

                        <br />
                    </div>
                </form>
            </div>
        </>
    );
};

export default ProfileDetails;
Enter fullscreen mode Exit fullscreen mode

ProfileDetails.test.js

import { screen, render } from '@testing-library/react';

import ProfileDetails from './ProfileDetails';

describe('<ProfileDetails />', () => {
    it('has a gender select heading', () => {
        render(<ProfileDetails />);

        const el = screen.getByText(/Select your gender/i);

        expect(el).toBeTruthy();
    });
});
Enter fullscreen mode Exit fullscreen mode

Sidebar.css

aside {
    background-color: rgba(255, 255, 255, 0.15);

    backdrop-filter: blur(10px);

    padding: 2rem;

    width: 100%;

    max-width: 16rem;

    border-top-left-radius: 8px;

    border-bottom-left-radius: 8px;

    height: 43.4rem;
}

.profile-sidebar-container {
    display: flex;

    flex-flow: row nowrap;

    justify-content: space-between;
}

.profile-image {
    background: #7e7dd6;

    border-radius: 100%;

    padding: 1rem;

    height: 2rem;
}

.profile-user p {
    font-size: 1rem;
}

.profile-user h1 {
    font-size: 1.6rem;
}

.settings {
    display: flex;

    flex-flow: row nowrap;

    justify-content: center;

    align-items: center;

    background: #ffffff;

    padding: 0.5rem;

    height: 2rem;

    width: 2rem;

    border-radius: 0.5rem;

    border: none;

    cursor: pointer;
}

aside {
    display: flex;

    flex-flow: column nowrap;

    justify-content: space-between;
}

aside nav,
.support-log-out {
    display: flex;

    flex-flow: column nowrap;
}

aside nav a,
.support-log-out a {
    color: rgb(43, 43, 43);

    text-decoration: none;

    font-weight: 600;

    padding: 0.4rem;

    border-radius: 0.2rem;
}

aside nav a:hover,
.support-log-out a:hover {
    background-color: #ffffff;
}
Enter fullscreen mode Exit fullscreen mode

Sidebar.js

import './Sidebar.css';

const Sidebar = () => {
    return (
        <>
            <aside>
                <div className="profile-sidebar-container">
                    <div className="profile-image">😎</div>

                    <div className="profile-user">
                        <p>Welcome back,</p>

                        <h1>Eren Yeager</h1>
                    </div>

                    <button className="settings">βš™οΈ</button>
                </div>

                <nav>
                    <a href="/" data-testid="search">
                        πŸ” Search
                    </a>

                    <a href="/" data-testid="dashboard">
                        🏠 Dashboard
                    </a>

                    <a href="/" data-testid="assets">
                        πŸ’· Assets
                    </a>

                    <a href="/" data-testid="business">
                        πŸ’Ό Business
                    </a>

                    <a href="/" data-testid="data">
                        πŸ“ˆ Data
                    </a>

                    <a href="/" data-testid="backups">
                        πŸ›  Backups
                    </a>
                </nav>

                <div className="support-log-out">
                    <a href="/" data-testid="support">
                        πŸ’¬ Support
                    </a>

                    <a href="/" data-testid="log-out">
                        β‡₯ Log Out
                    </a>
                </div>
            </aside>
        </>
    );
};

export default Sidebar;
Enter fullscreen mode Exit fullscreen mode

Sidebar.test.js

import { screen, render } from '@testing-library/react';

import Sidebar from './Sidebar';

describe('<Sidebar />', () => {
    it('has a search link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('search');
    });

    it('has a dashboard link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('dashboard');
    });

    it('has a assets link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('assets');
    });

    it('has a business link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('business');
    });

    it('has a data link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('data');
    });

    it('has a backups link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('backups');
    });

    it('has a support link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('support');
    });

    it('has a log-out link', () => {
        render(<Sidebar />);

        const el = screen.getByTestId('log-out');
    });
});
Enter fullscreen mode Exit fullscreen mode

Next run the commands below in your command line application but in different tabs/windows. So now you should have React, Jest and Cypress running at the same time. You might need to press a or enter to run all Jest tests.

# To start React
npm run start
Enter fullscreen mode Exit fullscreen mode
# To start Jest
npm run test
Enter fullscreen mode Exit fullscreen mode
# To start Cypress
npx cypress open
Enter fullscreen mode Exit fullscreen mode

Unit Tests and Integration Tests

You can find all of the example Unit and Integration Tests inside of the component folders. All of the tests should be passing you can play around with the files to see the tests fail and pass.

End-To-End Tests

The End-To-End tests are inside of my-app/cypress/integration/user.spec.js. To run the tests go to the Cypress application window and click on the button to run the test. If you click on that dropdown menu that has Electron as an option you will be able to select different web browsers.

https://res.cloudinary.com/d74fh3kw/image/upload/v1651347203/cypress_w7gdrn.png

The user.spec.js integration test automatically fills out the form and then the save button is clicked. A string version of the object that was created is output at the bottom of the page.

So lets do a quick recap you now know how to create:

  • Unit Tests
  • Integration Tests
  • End-To-End Tests

This was just a quick introduction take a look at the official documentation for Jest, React Testing Library and Cypress to learn more.

Discussion (1)

Collapse
yuridevat profile image
π•π•¦π•π•šπ•’ πŸ‘©πŸ»β€πŸ’»

Thank you so much Andrew for this amazing article on testing. It is really hard for me to get into this topic but your article made thinks so much clearer for me.