В каждом тестовом примере нам необходимо подготовить данные для теста. Очень часто мы говорим о 3 шагах тестов: Организовать, Действуть и Утверждать. Сегодня мы попробуем улучшить первый шаг: Аранжировка, используя Factory Pattern.
Пример проблемы
Нам нужно подготовить E2E-тесты для конечной точки API.
- Эта конечная точка создает контракты.
- Сервер должен ответить кодом 200 с новыми данными контракта после отправки правильных данных контракта.
- Неверный запрос с сообщением об ошибке после неправильных данных.
- Данные контракта должны включать два поля: ownerId и position.
- Данные пользователя должны включать адрес электронной почты и пароль.
- Если пользователь не существует, сервер должен ответить ошибкой.
Похоже на повседневную работу. В этом примере я использовал API GraphQL, но это не имеет значения, поскольку сегодня мы сосредоточимся на этапе упорядочения данных.
Хорошо, что нам нужно сделать:
Наш код будет выглядеть так:
it('Creates new Contract sending CORRECT DATA', async () => { //arrange step: const user = await prisma.user.create({ data: { email: "[email protected]", password: `#Password1A` } }); const contract = { ownerId:user.id , position:"position" }; // we dont care about this code for now //act step: const { status, body } = await graphQlMutation({ operation, variables: { ownerId: { value: user.id, }, position: { value: contract.position, }, }, fields }); const { data } = body; //assertion step: expect(status).toBe(200); //other assertions
Давайте сосредоточимся на этапе организации:
//arrange step (similar code will appear in many tests): const user = await prisma.user.create({ data: { email: "[email protected]", password: `#Password1A` } }); const contract = { ownerId:user.id , position:"position" };
Здесь мы видим 2 проблемы:
- данные жестко запрограммированы
- вполне вероятно, что мы сломаем DRY, повторяя один и тот же код во многих тестах.
Решение проблемы Проблема с жестко закодированными данными: фейкер
Что не так с жестко запрограммированными данными в тестах? Даже если ваши коды работают с адресом «[email protected]», вы не можете быть уверены, что они будут работать с другими адресами электронной почты.
Здесь я рекомендую вам этот пакет: https://www.npmjs.com/package/@faker-js/faker
Он может создавать фиктивные данные для ваших тестов. Это будет очень полезно на наших заводах.
npm i @faker-js/faker
и положи
//On top of our test // ESM import { faker } from '@faker-js/faker'; // or if you use require const { faker } = require('@faker-js/faker');
Теперь, с использованием Faker, наш шаг аранжировки будет выглядеть так:
const user = await prisma.user.create({ data: { email: faker.internet.email(), password: faker.internet.password(5) } }); const contract = { ownerId:user.id , position: faker.name.jobTitle(), };
Мы исправили проблему 1 (жестко закодированные данные), но проблема 2 все еще остается (DRY).
Решение проблемы 2: Фабричный шаблон
Чтобы избежать повторения этого кода снова и снова, мы можем переместить часть кода в новый класс в новом файле. Новый класс должен иметь 3 метода:
- генерировать — для генерации фейковых данных без сохранения
- create — для генерации и сохранения данных
- save — просто для сохранения данных
Зачем нам 3 функции? Иногда нам нужно просто сгенерировать данные для передачи их в API (например, данные контракта). Иногда нам нужно, чтобы эти данные находились в БД (например, пользовательские данные). Метод сохранения может пригодиться, например, когда мы решим изменить ORM.
Начнем с создания UserFactory.
Вы можете создать фабричный каталог в своем testdir ,или, если у вас есть тесты в том же каталоге, что и код — как и я, вы можете хранить фабрики рядом с кодом, который хотите протестировать. .
export class UserFactory{ static generate() { return { email: faker.internet.email(), password: faker.internet.password(5) }; } }
- Использование ключевого слова static здесь имеет некоторые преимущества: нам не нужно использовать ключевое слово new только для использования этого класса.
- Метод генерации просто создаст данные и не сохранит их в базе данных.
Давайте создадим метод «сохранить»:
export class UserFactory { static generate() { return { email: faker.internet.email(), password: faker.internet.password(5) }; } static save(userData) { return prisma.user.create({ data: { email: userData.email, password: userData.password } }); } }
- Метод save просто сохраняет данные в базу данных.
- Я использую здесь Prisma, вместо этого вам, конечно, следует использовать ORM.
Последний метод — «создать», он сгенерирует и сохранит данные:
export class UserFactory { static create(user?) { const userData = user || UserFactory.generate(); return UserFactory.save(userData); } static generate() { return { email: faker.internet.email(), password: faker.internet.password(5) }; } static save(userData) { return prisma.user.create({ data: { email: userData.email, password: userData.password } }); } }
- Метод create имеет 1 необязательный аргумент: user. Благодаря этому вы можете сохранять данные, сгенерированные методом «генерации», или передавать свои данные
- обратите внимание: вместо этого мы используем UserFactory — поскольку это статический метод
Теперь нам предстоит сделать то же самое с ContractFactory, я пропущу все шаги, так как это выглядит так же, как и для нашей UserFactory.
export class ContractsFactory { static create(ownerId: string) { const contractDate = ContractsFactory.generate(ownerId); return ContractsFactory.save(contractDate); } static generate(ownerId: string) { return { position: faker.name.jobTitle(), ownerId }; } static save(contractDate) { return prisma.contract.create({ data: contractDate }); } }
- как вы можете видеть, метод «generate» имеет аргумент OwnerId — поэтому вы можете передать туда userId.
Код после применения Factory Pattern:
it('Creates new Contract sending CORRECT DATA', async () => { //arrange step: const user = await UserFactory.create(); const contract = ContractsFactory.generate(user.id); // we dont care about this code for now //act step: const { status, body } = await graphQlMutation({ operation, variables: { ownerId: { value: user.id, }, position: { value: contract.position, }, }, fields }); const { data } = body; //assertion step: expect(status).toBe(200); //other assertions
Более того, код намного короче — вы можете использовать эти фабрики в других тестах, например BadRequest, поскольку несуществующий пользователь будет выглядеть так:
it('BAD REQUEST sending NOT EXISTING USER_ID', async () => { const contract = ContractsFactory.generate('not existing id'); const { status, body } = await graphQlMutation({ operation, variables: { ownerId: { value: 'not existing user', required: true }, position: { value: contract.position, required: true }, }, fields, }); // assertions here
Что еще мы можем получить, используя фабрики?
Мы сделали код короче, но у фабрик есть гораздо больше преимуществ:
- мы можем добавить к нашим данным больше свойств, изменив всего один файл. например :
static generate(ownerId: string) { const contract = { id: faker.datatype.uuid(), position: faker.name.jobTitle(), startDate: faker.date.past(), endDate: faker.date.future(), vacationDaysPerYear: faker.datatype.number(), vacationDays: faker.datatype.number(), ownerId };
Без использования фабрик нам придется редактировать весь тест только для того, чтобы добавить к объекту одно свойство, здесь мы отредактировали только 1 файл.
- мы можем использовать его в сеялках
- мы можем добавить больше методов, например метод, который генерирует неправильные данные, слишком короткий пароль и многое другое.
А как насчет использования фабрик в… сеялках?
Хороший вопрос, и да, это потрясающая идея. Очень часто нам нужна исходная база данных с поддельными данными — например, в среде «dev». Мы можем использовать для этого созданные нами фабрики.
Например, вы можете сделать так:
- создайте новый файл: src/index.seed.ts
- внутри:
function createUser() { const userData = UserFactory.generate(); return UserFactory.save(userData); } function createContract(ownerId: string) { const userData = ContractsFactory.generate(ownerId); return ContractsFactory.save(userData); } async function main() { const user = await createUser(); console.log(user); const contract = await createContract(user.id); console.log(contract); // many other seeds } main();
- в package.json:
"scripts": { "seed": "NODE_ENV=development ts-node -r tsconfig-paths/register src/index.seed.ts", }
- вам необходимо подключиться к базе данных. Сидеры не будут использовать соединения из тестов. Способ подключения к БД зависит от вашей базы данных/орма, в MongoDB, Prisma и TypeOrm все будет по-другому, однако я делаю что-то вроде этого:
export class AbstractFactory { static prisma = PrismaServiceFactory.create(); }
и добавьте «расширяет AbstractFactory» в каждый файл, это будет что-то вроде:
export class ContractsFactory extends AbstractFactory { static create(ownerId: string) { const contractDate = ContractsFactory.generate(ownerId); return ContractsFactory.save(contractDate); } static generate(ownerId: string) { const contract = { id: faker.datatype.uuid(), position: faker.name.jobTitle(), startDate: faker.date.past(), endDate: faker.date.future(), vacationDaysPerYear: faker.datatype.number(), vacationDays: faker.datatype.number(), ownerId }; return contract; } static save(contractDate) { return this.prisma.contract.create({ data: contractDate }); } }