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