В каждом тестовом примере нам необходимо подготовить данные для теста. Очень часто мы говорим о 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
        });
    }
}