Мне доставляет удовольствие изучать Node.js и глубже изучать тестирование. Тестирование — одна из самых важных частей написания кода, вы никогда не будете писать код без ошибок. Из различных типов тестирования модульное тестирование является одним из них. В этой статье я объясню, как реализовать модульное тестирование в Node.js.
Введение
Давайте немного поговорим о бэкенд-разработке в Курио, мы используем Node.js в качестве вторичного языка программирования для создания некоторых небольших сервисов, мы не используем его как сервис, подключенный напрямую к базе данных. Каждый созданный нами сервис Node.js имеет три разных уровня: обработчик, сервис и репозиторий.
root ├── src │ └── handlers │ └── repositories │ └── services │ └── app.js ├── test
- Handler
Handler или HTTP-обработчик — это функция, которая будет выполнять запрос при совпадении маршрута. Этот уровень отвечает за определение вывода, который будет возвращен клиенту. - Служба
Служба – это уровень, который контролирует бизнес-логику. - Репозиторий
Репозиторий — это уровень, который напрямую связан с базой данных или HTTP-вызовом, задача которого заключается в управлении данными.
Подготовка
Создайте корень проекта и установите необходимые модули. Для модуля HTTP-запросов мы используем axios, express для маршрутизации, а для модульного тестирования у нас есть mocha, chai, nock и sinon.
npm install --save axios express npm install --save-dev mocha chai nock sinon
В этой статье я не хочу говорить об аксиомах и экспрессах, инициации сервера или маршрутизации, я предполагаю, что вы, ребята, уже знаете об этом. Давайте поговорим о мокко, чае, нок и синоне.
Мокко
Mocha — это тестовый фреймворк JavaScript, то есть это инструмент, который выполняет наши тесты. Другие популярные тестовые фреймворки, такие как Jest и Jasmine. Вот пример использования мокко.
it("first testing", function () { // your code });
it("second testing", function () { // your code });
Чтобы запустить тест mocha, вы можете запустить двоичный файл mocha внутри node_modules.
./node_modules/mocha/bin/mocha
Если вы устанавливаете его глобально, вам нужно только выполнить mocha
.
Чай
Chai — это библиотека утверждений. Мы используем это, чтобы утверждать, что следует делать при тестировании. Например, мы ожидали, что функция должна вернуть 1.
const expect = chai.expect; expect(foo).to.equal(1);
Нок
Nock — библиотека для имитации HTTP-сервера. Nock можно использовать для тестирования модулей, которые делают HTTP-запросы отдельно. Вот пример того, как мы использовали nock для имитации HTTP-запроса.
const scope = nock("http://www.example.com") .get("/resource") .reply(200, "your-mock-data")
СинонJS
SinonJS — это библиотека, которая предоставляет автономные тестовые шпионы, заглушки и макеты. Хм, какие они? Итак, в основном SinonJS имеет три основные функции.
- Шпионы
Шпионы создают поддельные функции, которые мы можем использовать для отслеживания казней. Это означает, что мы можем узнать, вызывалась ли функция или сколько раз функция выполнялась. - Заглушки
Заглушки управляют поведением метода, что означает, что вы можете управлять тем, что возвращается, или вызывать ошибку. - Моки
Моки определяют, как мы ожидаем, что функция будет работать, и используютmock.verify()
, чтобы убедиться, что она работает так, как мы ожидаем.
В этой статье я не буду использовать функцию mocks. Если вы хотите изучить его, вы можете посетить https://sinonjs.org/releases/latest/mocks/.
Давай попрактикуемся
Хорошо, теперь мы собираемся больше сосредоточиться на модульном тестировании. Я сделал сценарий, мы получим данные профиля компании. Данные будут содержать название компании, список пользователей и сотрудников.
Сначала я создал репозиторий компании. В этом репозитории я извлек данные с помощью вызовов HTTP.
/src/repositories/company.repository.js
Во-вторых, я создал сервис компании. В этом сервисе я подобрал id
и name
пользователя и сотрудника и слил в один и тот же объект.
/src/services/company.service.js
Наконец, это обработчик компании. Ничего особенного, я просто вернул данные профиля компании в виде JSON.
/src/handlers/company.handler.js
Это наша иерархия папок.
root ├── src │ ├── handlers │ │ └── company.handler.js │ ├── repositories │ │ └── company.repository.js │ ├── services │ │ └── company.service.js │ └── app.js ├── test
Время тестирования
Итак, теперь мы собираемся протестировать эти слои, давайте создадим папку с именем test
, а затем создадим тестовый файл для каждого слоя. Я также создал два файла для хранения фиктивных данных пользователя и сотрудника.
root ├── src ├── test ├── mocks │ └── employees.js │ └── users.js ├── company.handler.test.js ├── company.service.test.js └── company.repository.test.js
/test/mocks/users.js
module.exports = [ { "login": "mojombo", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4", "gravatar_id": "", "type": "User", "site_admin": false }, { "login": "defunkt", "id": 2, "node_id": "MDQ6VXNlcjI=", "avatar_url": "https://avatars0.githubusercontent.com/u/2?v=4", "gravatar_id": "", "type": "User", "site_admin": false } ];
/test/mocks/employees.js
module.exports = { "status": "success", "data": [ { "id": "1", "employee_name": "Tiger Nixon", "employee_salary": "320800", "employee_age": "61", "profile_image": "" }, { "id": "2", "employee_name": "Garrett Winters", "employee_salary": "170750", "employee_age": "63", "profile_image": "" } ] };
Протестируйте репозиторий
В репозитории мы создали метод fetchUsers
для получения списка пользователей по HTTP-вызову.
async function fetchUsers() { const { data } = await axios.get("https://api.github.com/users"); return data; }
Как мы это проверим? Вот как я тестирую репозиторий.
/test/company.repository.test.js
При тестировании репозитория мы реализуем nock
для имитации HTTP-запроса, это означает, что мы берем под контроль возвращаемый HTTP-запрос.
it("should succeed", async function () { const usersScope = nock("https://api.github.com") .get("/users") .reply( 200, usersMock, { 'Content-Type': 'application/json' } ); ... });
Мы указываем, что эта конечная точка должна возвращать статус 200
, что означает успех, и ответ usersMock
(/test/mocks/users.js).
Затем мы ожидаем, что должен вернуть fetchUsers
. usersScope.done()
используется для выдачи ошибки утверждения, если при этом HTTP-запрос не был выполнен.
it("should succeed", async function () { ... const users = await companyRepository.fetchUsers(); expect(users).to.have.lengthOf(2); expect(users).to.deep.equal(usersMock); expect(users[0].login).to.equal("mojombo"); usersScope.done(); });
Протестируйте сервис
В сервисе мы делаем простую обработку данных.
... async function fetchCompanyProfile() { const users = await companyRepository.fetchUsers(); const employees = await companyRepository.fetchEmployees(); const companyUsers = []; for (let user of users) { companyUsers.push({ id: user.id, name: user.login, }); } const companyEmployees = []; for (let employee of employees) { companyEmployees.push({ id: parseInt(employee.id), name: employee.employee_name, }); } return { companyName: "Facebook Inc", companyUsers, companyEmployees, }; } ...
Как мы это проверим? Вот как я тестирую сервис.
/test/company.service.test.js
В этом тестировании мы реализуем sinon.stub
для имитации функций fetchUsers
и fetchEmployees
.
describe("test fetch company profile", function () { it("should succeed", async function () { const fetchUsersFn = stub(companyRepository, "fetchUsers") .returns(usersMock); const fetchEmployeesFn = stub( companyRepository, "fetchEmployees" ).returns(employeesMock.data); ...
Функция fetchUsers
возвращает usersMock
(/test/mocks/users.js), а fetchEmployees
возвращает employeesMock.data
(/test/mocks/employees.js).
Вот что мы ожидаем, что fetchCompanyProfile
будет возвращено, и ожидается, что fetchUsers
и fetchEmployees
будут вызваны.
... expect(fetchUsersFn.calledOnce).to.be.true; expect(fetchEmployeesFn.calledOnce).to.be.true; const expectedData = { companyName: 'Facebook Inc', companyUsers: [ { id: 1, name: 'mojombo' }, { id: 2, name: 'defunkt' }, ], companyEmployees: [ { id: 1, name: 'Tiger Nixon' }, { id: 2, name: 'Garrett Winters' }, ], }; expect(companyProfile).to.deep.equal(expectedData);
Протестируйте обработчик
В функции-обработчике мы возвращаем данные только в формате JSON для клиента.
... async function fetchCompanyProfile(req, res) { try { const companyProfile = await companyService.fetchCompanyProfile(); res.json(companyProfile); } catch (e) { errorResponse(e, res); } } ...
Это наш тест обработчика.
/test/company.handler.test.js
Мы делаем что-то похожее на служебный тест, имитируя функцию fetchCompanyProfile
с помощью stub
. Проблема здесь в том, как я издеваюсь над запросом и ответом с помощью spy
. Поскольку мы используем res.json()
для возврата ответа JSON клиенту, нам нужно только заменить реальную функцию на sinon.spy()
. Spy на самом деле не дает нам контроля над функцией, он только дает нам фиктивную функцию, которую мы можем использовать для отслеживания выполнения функции.
const req = {}; const res = { json: spy(), }; await companyHandler.fetchCompanyProfile(req, res); expect(fetchCompanyProfileFn.calledOnce).to.be.true; expect(res.json.calledOnce).to.be.true; expect(res.json.firstCall.args[0]).to.deep .equal(companyProfileMock);
Запустить тест
Чтобы запустить тест, вы можете установить команду test
в поле scripts
в package.json, вызвав двоичный файл mocha из node_modules.
/package.json
"scripts": { "test": "./node_modules/mocha/bin/mocha" },
После этого запустите npm run test
в командной строке.
➜ jstesting git:(master) ✗ npm run test > [email protected] test /Users/rezaindra/Desktop/nodecode/KurioApp/jstesting > ./node_modules/mocha/bin/mocha test company handler ✓ should succeed ✓ should be error test company repository test fetch users ✓ should succeed ✓ should be error test fetch employees ✓ should succeed ✓ should be error test company service test fetch company profile ✓ should succeed ✓ should be error when fetching users ✓ should be error when fetching employees 9 passing (44ms)
Вывод
Наконец, мы узнали, как реализовать модульное тестирование для каждого уровня в приложении Node.js с помощью полезных библиотек тестирования. Что дальше? Вы можете спросить меня об этом в комментарии, если у вас есть проблема или что-то, чего вы не понимаете. Или вы можете узнать полностью, загрузив этот репозиторий проекта и изучив его.
Удачного тестирования!