Я обожаю писать программы. Да что уж там: я не могу представить себя, занимающимся чем-то другим. Шутки шутками, но есть у меня один страх, который свойственен всем энтузиастам в программировании. И страх этот называется «не сдать»: опоздать по срокам, не оправдать ожиданий, не уложиться в бюджет.
Вот в чем проблема: поставка — это слабое звено всего сообщества разработчиков. Ниже приведено исследование Standish Group о создании приложений.
Цифры ужасают! Большая часть проектов сдавалась годами позже, не вписывалась в бюджет и даже не соответствовала заявленному назначению. Если сложить все вместе, то получается, что лишь 36% проектов оказались успешными. Это говорит о том, что искусству разработки программ явно не хватает надежности.
Хорошая новость: для разработчиков придумали разные инструменты. В данной статье мы рассмотрим автоматизацию тестирования и обсудим полезную концепцию под названием «тестовое покрытие».
Да, но зачем мне нужны автоматизированные тесты?
Для тех из вас, кто не оценил всех прелестей автоматизированного тестирования, говорю: автоматические тесты (например, модульное тестирование) нужны для фильтрации багов и ошибок. Если и есть что-то, с чем отлично справляется любой разработчик, — так это создание багов и поломка работающего софта. И, как сказал герой Билл Палмер из книги «Проект «Феникс»:
Покажите мне разработчика, который не разрушает производственные системы, и, могу спорить, он просто мертв. Или скорее в отпуске.
Тестируйте код, программируйте тесты
Без реальных примеров кода все вышесказанное — демагогия. Для начала создадим Nest.js приложение. Nest.js — это отличный фреймворк для создания модульного бэкенд приложения на TypeScript. Такое приложение состоит из модулей Controller и Service, а также использует TypeORM. В общем, все, что нужно разработчику для быстрой поставки качественного софта. Почитайте сайт Nest— документация просто потрясающая.
Итак, я установил Nest.js CLI и написал в терминале следующее:
nest new testproject
Эта команда создает новый проект. Подробнее об архитектуре кода смотрите в документации. Сейчас давайте обратим внимание на файлы Controller и Service. Мы будем использовать TypeORM, которая абстрагирует запросы к базе данных. Давайте начнем.
Поговорим о простой конечной точке, которая регистрирует пользователей. Эта точка обладает следующими бизнес-правилами:
- Она получает регистрацию пользователей.
- Проверяет, есть ли в базе другой пользователь с тем же адресом электронной почты. Если такой пользователь обнаруживается, то возвращает ошибку.
Проще простого, да? Вот и не будем ничего усложнять, а лучше займемся непосредственно тестированием. Для создания тестов я выбрал методику TDD (разработку через тестирование). Она действительно помогает поставлять более качественные проекты.
Разберем структуру документа:
export class User { id?: string; email: string; name: string; password?: string;}
По сути не очень сложно. Всего лишь 4 переменные: ID (создается после регистрации в базе данных), почтовый адрес, имя и пароль, который задается в отправленном письме. Эти подробности мы опустим и будем считать, что вызываем функцию, которая обрабатывает e-mail. Теперь посмотрим на Service.
const checkUser = this.userRepos.findOne(user.email); if (checkUser) { throw new UserAlreadyExistsError('email', user.email); } const insertedUser: any = this.userRepos.create(user); const statusEmail = await sendEmail(); if(!statusEmail) { throw new Error('Email cant be sent'); } return await insertedUser.save(); }
Здесь все предельно ясно — этот модуль проверяет, есть ли такой пользователь в базе данных. Если да, то выдает ошибку о том, что такой пользователь уже существует. Настоятельно рекомендую обрабатывать ошибки в виде абстрактной Domain Error, а не выбрасывать ошибки HTTP. HTTP — это протокол передачи данных, и он должен обрабатываться только в файле Controller. Если пользователь не найден, то контроллер добавляет его в базу данных и отправляет e-mail. Я не буду описывать функцию sendmail. Давайте считать, что она возвращает true, а если что-то пошло не так, то возвращается false, но сам пользователь не удаляется из базы данных.
Вот код для теста:
it('should create a User', async () => { repositoryMock.create.mockReturnValue({ save: () => mockedUser}); repositoryMock.findOne.mockReturnValue(undefined); expect(await service.insertUser(mockedUser as User)).toEqual(mockedUser); // Проверка того, что обычно «заглушенные» методы вызываются с параметрами expect(repositoryMock.create).toHaveBeenCalledWith(mockedUser); }); });
Тест также очень простой. Обратите внимание, что в тестах фигурирует две заглушки функций, а именно TypeORMфункции createи findOne. Эта отличная практика помогает не заполонять базу данных тестами. Всегда создавайте mock-функции базы данных. А если вы захотите протестировать взаимодействие приложения с БД, то напишите для этого отдельный тест.
Покрытие кода тестами
Итак, мы успешно протестировали функцию по добавлению пользователя, а также провели мокинг функций базы данных… осталось обсудить еще одну важную концепцию — покрытие тестами (jest — coverage). Запуская команду для файла user.service, мы получаем следующий результат:
Мы протестировали нашу функцию. Она работает, но две строки остались не покрытыми тестами. Все это отражается в процентном значении столбцов Statement, Branch и Line. Давайте рассмотрим первую строку:
if (checkUser) { throw new UserAlreadyExistsError('email', user.email); }
В ней нет покрытия, поскольку мокинг функции findOne вернул неопределенного пользователя. Таким образом, покрытие тестами для данной условной конструкции невозможно. Во второй строке прослеживается та же тенденция. Получается, что нам нужно покрытие тестами операторов if! Но как это сделать? Мы уже проводили мокинг функций базы данных. Все, что нам нужно, — выполнить мокинг других функций, которые будут возвращать то, что от них ожидают условные конструкции. Вернемся к самому тестированию.
it('should return an Error', async () => { repositoryMock.create.mockReturnValue({ save: () => mockedUser}); repositoryMock.findOne.mockReturnValue(mockedUser); await expect(service.insertUser(mockedUser)).rejects.toBeInstanceOf(UserAlreadyExistsError) });
Если внимательно присмотреться, то можно заметить, что единственная разница в коде этого теста — мокинг findOne. При изменении этой функции она попадет в цикл if, и наш файл кода полностью покрывается тестами. Давайте проверим:
Готово. Мы детально проработали функцию, протестировали ее со всех сторон и создали более качественный код. На такую работу требуется время, но оно того стоит. Всегда программируйте тесты и тестируйте код. Весь код можно найти на GitHub. Там же есть и файл controller. Вы можете попрактиковаться, придумав для него свои тесты.
Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming