001 Типы тестирования
e2e тесты можно проводить как на АПИ, так и через браузер (фронт-приложение)
Тестирование АПИ:
- Jest
- Mocha
- Chai
Тестирование фронта:
- Cypress
- Protractor
- Selenium
Для интеграционного тестирования нам не обязательно поднимать всё приложение. Тут достаточно поднять два инстанса
Для юнита нужно лишь вписать тесты под отдельный модуль
002 Тесты отзывов
e2e тесты хранятся в папке test
. Там же находится файл настроек, где и указано, какие тесты должен запускать Jest
Далее уже в той же папке лежит и сам e2e тест приложения.
-
describe
- описывает запускаемую группу тестов -
beforeEach
- функция, которая выполняется до каждого отдельного теста -
beforeAll
- выполняется один раз перед всеми тестами -
afterAll
- выполняется один раз после всех тестов -
it
- описывает действия на каждом отдельном тесте -
app
- переменная, которая получает из moduleFixture (переменная с модулями для теста) всё целиковое приложение -
Test.createTestingModule
- фиксирует ровно те модули в приложении, которые нам нужно запустить (конкретно тут вызвается всё приложение) -
request
- маленькая библиотечка из supertest, которая позволяет удобно общаться с локальным АПИ
test > review.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
// describe описывает группу тестов
describe('AppController (e2e)', () => {
// это всё приложение неста
let app: INestApplication;
// данный код будет выполняться перед каждым запуском выполнения следующего теста
beforeEach(async () => {
// создаёт отдельный тестовый модуль
const moduleFixture: TestingModule = await Test.createTestingModule({
// конкретно тут собирается и импортируется всё приложение
imports: [AppModule],
}).compile(); // компиляция
// создаём уже само приложение неста
app = moduleFixture.createNestApplication();
// инициализация приложения
await app.init();
});
// выполняет отдельный кейс теста
it('/ (GET)', () => {
return request(app.getHttpServer()) // получаем http-сервер приложения
.get('/') // отправляем запрос на индекс-роут
.expect(200) // ожидаем 200
.expect('Hello World!'); // ожидаем приветствие
});
});
Далее напишем несколько тестов. Хорошей практикой будет писать тесты как на успешное выполнение операции, так и на выполнение с ошибкой.
Внутри it
:
- пишем имя теста по определённой нотации
- если мы будем использовать внутри ответа
then
, то функция должна бытьasync
- первым делом, мы через
request(app.getHttpServer())
получаем доступ к нашему локальному хосту для запросов - далее у нас идёт сам запрос (
post
,get
,delete
и так далее). В него всталяем роут запроса (без использования пути относительно глобального префикса черезapp.setGlobalPrefix()
, так как будет ошибка) - дальше мы можем вписать
send()
, если нам нужно что-то отправить на сервер - дальше вписать
expect()
, в котором находится ожидаемый ответ от сервера - дальше уже можно поместить
then((res: request.Respone) => {})
, в котором нужно провести определённые операции проверки (expect()
)
test > review.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { CreateReviewDto } from '../src/review/dto/create-review.dto';
import { disconnect, Types } from 'mongoose';
import { REVIEW_NOT_FOUND } from '../src/review/review.constants';
const productId = new Types.ObjectId().toHexString();
const testDto: CreateReviewDto = {
name: 'Olek',
rating: 3.5,
title: '',
description: '',
productId,
};
describe('AppController (e2e)', () => {
let app: INestApplication;
let createdId: string; // id созданного объекта
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
// передаём запрос на создание нового обзора
it('/review/create (POST) - success', async () => {
return request(app.getHttpServer())
.post('/review/create')
.send(testDto) // отправляем объект на сервер
.expect(201)
.then(({ body }: request.Response) => {
// присваиваем id ответа
createdId = body._id;
// описываем, что мы ожидаем наличие значения
expect(createdId).toBeDefined();
});
});
// передаём запрос на получение нового обзора - успешный запрос
it('/review/getByProduct/:productId (GET) - success', async () => {
return request(app.getHttpServer())
.get('/review/getByProduct/' + productId)
.expect(200)
.then(({ body }: request.Response) => {
// так как нам приходит массив из одного элемента, то длина должна быть = 1
expect(body.length).toBe(1);
});
});
// передаём запрос на получение нового обзора, но ответ будет с ошибкой
it('/review/getByProduct/:productId (GET) - fail', async () => {
return request(app.getHttpServer())
.get('/review/getByProduct/' + new Types.ObjectId().toHexString())
.expect(200)
.then(({ body }: request.Response) => {
// тут нам уже должен прийти пустой массив
expect(body.length).toBe(0);
});
});
// передаём запрос на удаление нового обзора
it('/review/:id (DELETE) - success', () => {
return request(app.getHttpServer())
.delete('/review/' + createdId)
.expect(200);
});
// передаём запрос на удаление нового обзора, но с ошибкой
it('/review/:id (DELETE) - fail', () => {
return (
request(app.getHttpServer())
.delete('/review/' + new Types.ObjectId().toHexString())
// ожидаем получить 404 NOT_FOUND
.expect(404, {
statusCode: 404,
message: REVIEW_NOT_FOUND,
})
);
});
// после всех тестов
afterAll(() => {
// отключаемся от БД
disconnect();
});
});
003 Unit тесты
И изначально при создании любого объекта через нест, у нас создаются начальные тесты
src > review > review.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ReviewService } from './review.service';
describe('ReviewService', () => {
let service: ReviewService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ReviewService],
}).compile();
service = module.get<ReviewService>(ReviewService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
Однако они работать не будут, так как в тесты нужно ещё будет вложить все зависимости, которые инжектятся в сервис
Для исправления вышеописанной проблемы нужно будет замокать все зависимости
Через getModelToken('ReviewModel')
мы просим тайпгуз, чтобы он от зависимости ReviewModel
нашёл её токен и по нему нужно будет заинжектить некоторую новую фабрику
Далее нам нужно описать работу reviewRepositoryFactory
. Он возвращает объект с функциями. Чтобы работал чейн, как в оригинальных функциях, нужно, чтобы его функции возвращали exec
. Сам exec
представляет из себя объект, который хранит функцию-заглушку jest.fn()
Теперь мы можем воспользоваться большим количеством моковых данных
И примерно так выглядит реализация unit-теста с с фабрикой, которая генерирует нам моковые методы:
src > review > review.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ReviewService } from './review.service';
import { getModelToken } from 'nestjs-typegoose';
import { Types } from 'mongoose';
describe('ReviewService', () => {
let service: ReviewService;
const exec = { exec: jest.fn() };
const reviewRepositoryFactory = () => ({ find: () => exec });
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ReviewService,
{
// эта функция будет возвращать моковые зависимости для тестов
useFactory: reviewRepositoryFactory,
// здесь мы провайдим токен, который вставляем в модель
provide: getModelToken('ReviewModel'), // получаем токен указанной модели
},
],
}).compile();
service = module.get<ReviewService>(ReviewService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('findByProduct working', async () => {
// генерируем id
const id = new Types.ObjectId().toHexString();
// создаём моковые данные
reviewRepositoryFactory()
.find()
// возвращаем единоразово моковые данные с id продукта
.exec.mockReturnValueOnce([{ productId: id }]);
// ищем продукт по id в моковых данных
const res = await service.findByProductId(id);
// мы ожидаем, что свойство id продукта нулевого элемента будет = id
expect(res[0].productId).toBe(id);
});
});
Запуск unit- и screenshot-тестов:
npm run test