Что такое разработка через тестирование?

Test Driven Development или TDD для краткости — это, по сути, процесс, через который проходят разработчики и команды, когда они тестируют свой код. Кодирование, проектирование и тестирование объединяются вместе, и создаются тестовые примеры, чтобы гарантировать, что код был тщательно протестирован, а любые ошибки или ошибки были устранены на этапе разработки до того, как он достигнет производственного уровня.

Это считается хорошей практикой и методологией, которой должны следовать все разработчики при работе над кодовой базой. Благодаря этому процессу код со временем улучшается, что приводит к гораздо более стабильному приложению. В этой статье мы рассмотрим модульные тесты, интеграционные тесты и сквозные тесты.

Что такое модульные тесты?

По сути, модульный тест — это метод тестирования небольших образцов кода в приложении. Это могут быть функции, запускающие блоки кода, или API, которые возвращают данные. Цель состоит в том, чтобы выяснить, правильно ли работает код и перехватывает ли он какие-либо ошибки при их возникновении. Например, в форме возвращаются неверные данные.

Что такое интеграционные тесты?

Интеграционные тесты — это просто несколько сгруппированных модульных тестов. Таким образом, в то время как один модульный тест проверяет одну часть функциональности, интеграционный тест больше похож на набор тестов. Таким образом, в некотором смысле вы теперь тестируете несколько блоков кода одновременно, как, например, весь компонент карусели. Если бы это был модульный тест, то вы бы только тестировали, чтобы увидеть, загружается ли изображение, тогда как в тесте интеграции вы теперь проверяете, загружается ли заголовок, загружается ли изображение, отображаются ли правильные данные и т. д. Интеграция тесты отлично подходят для тестирования пользовательских потоков.

Что такое сквозные тесты?

Конечные тесты — это способ тестирования рабочего процесса внешнего интерфейса приложения. Это метод тестирования всего приложения, чтобы вы знали, что оно будет вести себя так, как вы ожидаете. Разница между сквозным тестированием и двумя другими заключается в том, что сквозное тестирование проверяет программное обеспечение и систему, тогда как два других больше предназначены для систематического тестирования.

Как провести тестирование?

Библиотека тестирования Jest и React чрезвычайно популярны, когда речь идет о выполнении модульных и интеграционных тестов в командной строке. Cypress — популярный инструмент для сквозного тестирования в браузере. Jest можно использовать даже на бэкэнде, чтобы вы могли охватить все свои базы и использовать одну и ту же библиотеку для тестирования бэкенда и внешнего интерфейса.

Библиотеки модульного/интеграционного тестирования

Библиотеки сквозного тестирования

Настройка проекта

Настроим наш проект. Перейдите в каталог на вашем компьютере, откройте командную строку и выполните приведенные ниже команды.

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}

Теперь запустите эту команду, чтобы запустить Cypress, вы должны увидеть окно Cypress, открытое на вашем компьютере.

# To start Cypress
npx cypress open

Существует множество примеров интеграционных тестов, если вы хотите, вы можете запустить их, чтобы увидеть, что они делают. Когда вы будете готовы, откройте проект в редакторе кода, зайдите внутрь своего проекта и найдите папку интеграции Cypress по адресу my-app/cypress/integration и удалите папки внутри нее, чтобы у нас был чистый лист.

Затем создайте файл с именем user.spec.js и поместите его в папку integration с приведенным ниже кодом. Это будет первый сквозной тест, но он еще не сработает, потому что в нашем приложении нет кода!

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('[email protected]');
        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();
    });
});

Наконец пришло время добавить код в файлы, которые мы создали ранее. Скопируйте и вставьте приведенный ниже код в соответствующие файлы. Это довольно утомительный процесс, потому что они разделены на компоненты, но в конце концов это того стоит.

В качестве альтернативы вы можете просто клонировать/загрузить репозиторий и сразу перейти к концу этой статьи, а именно к разделу Модульные и интеграционные тесты.

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

Файлы компонентов приложения

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;
}

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;

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');
    });
});

Файлы компонентов формы

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;
}

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;

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');
});
});

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;
}

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;

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');
    });
});

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;
}

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;

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();
    });
});

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;
}

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;

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();
    });
});

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;
}

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;

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');
    });
});

Затем запустите приведенные ниже команды в приложении командной строки, но в разных вкладках/окнах. Итак, теперь у вас должны быть одновременно запущены React, Jest и Cypress. Возможно, вам потребуется нажать a или enter, чтобы запустить все тесты Jest.

# To start React
npm run start
# To start Jest
npm run test
# To start Cypress
npx cypress open

Модульные тесты и интеграционные тесты

Вы можете найти все примеры модульных и интеграционных тестов в папках компонентов. Все тесты должны быть пройдены, вы можете поиграть с файлами, чтобы увидеть, как тесты проваливаются и проходят.

Сквозные тесты

Сквозные тесты находятся внутри my-app/cypress/integration/user.spec.js. Для запуска тестов перейдите в окно приложения Cypress и нажмите кнопку запуска теста. Если вы нажмете на это раскрывающееся меню, в котором в качестве опции есть Electron, вы сможете выбрать разные веб-браузеры.

Интеграционный тест user.spec.js автоматически заполняет форму, а затем нажимается кнопка сохранения. Строковая версия созданного объекта выводится внизу страницы.

Итак, давайте сделаем краткий обзор, который вы теперь знаете, как создавать:

  • Модульные тесты
  • Интеграционные тесты
  • Сквозные тесты

Это было просто краткое введение. Чтобы узнать больше, ознакомьтесь с официальной документацией для Jest, React Testing Library и Cypress.

Бит: почувствуйте мощь компонентно-ориентированной разработки

Скажи привет Bit. Это инструмент №1 для разработки приложений на основе компонентов.

С помощью Bit вы можете создать любую часть своего приложения в виде «компонента», который можно компоновать и использовать повторно. Вы и ваша команда можете совместно использовать набор компонентов для более быстрой и последовательной совместной разработки большего количества приложений.

  • Создавайте и компонуйте «строительные блоки приложения»: элементы пользовательского интерфейса, полные функции, страницы, приложения, бессерверные или микросервисы. С любым стеком JS.
  • С легкостью делитесь и повторно используйте компоненты в команде.
  • Быстро обновляйте компоненты в разных проектах.
  • Делайте сложные вещи простыми: Монорепо, дизайн-системы и микрофронтенды.

Попробуйте Bit бесплатно и с открытым исходным кодом→

Узнать больше