092 Виды тестирования

Тесты делятся на несколько типов:

  • unit - изолированные тесты компонентов. Пишутся в самом большом количестве, чтобы протестировать отдельные части системы. Данные тесты крайне просто и легко запустить
  • integrtation - тесты между компонентами системы. Такие тесты проводятся для проверки взаимодействия элементов системы - например, проверить парные сервисы, которые обмениваются между друг другом данными.
  • e2e - тесты на собранной системе. Тестируется работа сразу всего кода в определённом кружении. Обычно тестируется на продакшн-версии приложения.

Так же у нас имеются фреймворки для проведения тестирования:

093 Unit тесты

Устанавливаем jest

npm i -D jest @types/jest ts-jest

Далее создаём скрипт под запуск тестов

package.json

"scripts": {
	"test": "jest"
},

Далее создаём конфиг под модуль юнит-тестирования

jest.config.ts

// импортируем отдельно тип
import type { Config } from '@jest/types';
 
// конфигурация юнит-тестов
const config: Config.InitialOptions = {
	// чтобы видеть детальный output
	verbose: true,
	//
	preset: 'ts-jest',
};
 
export default config;

Дальше нам нужно написать сам сценарий тестирования.

beforeAll() - функция выполняется перед всеми тестами afterAll() - запускает определённое действие сразу после всех тестов beforeEach() - функция, которая будет выполняться перед каждым тестом describe() - описывает, что мы тестируем it() - это отдельный тест, который передаёт по нужному каналу определённое значение

Так же у нас в приложении имеется несколько сервисов, которые нужно отдельно поднимать, чтобы запустить тестировку (например, поднимать работу призмы, чтобы она создавала базу). Мы этого делать не хотим, поэтому нужно создать Mock под эти сервисы, чтобы использовать их функционал при тестировании без нужды в их включении.

Далее нужно будет в контейнере прибиндиться не к модулям, а к тем константам мокам, которые мы создали. Чтобы прибиндиться к константе нужно использовать не просто to(), а функцию toConstantValue(), которая работает с константами

И теперь, например, когда что-то в коде дёрнет внутри модуля UsersRepository метод create(), то у нас сработает определённый нами jest.fn().mockImplementationOnce(), который уже вернёт заранее определённое нами значение

Метод expect() получает определённое значение и позволяет нам указать то значение, которое мы от его ожидаем

Так же самым первым импортом обязательно нужно добавлять библиотеки зависимостей приложения. Конкретно в тестовый файл нужно импортнуть 'reflect-metadata'.

src > users > users.service.spec.ts

// обязательно нужно добавить сюда рефлект
import 'reflect-metadata';
 
import { Container } from 'inversify';
import { IConfigService } from '../config/config.service.interface';
import { IUsersRepository } from './users.repository.interface';
import { IUserService } from './users.service.interface';
import { TYPES } from '../types';
import { UserService } from './users.service';
import { User } from './user.entity';
import { UserModel } from '@prisma/client';
 
// поднимаем и вызываем срабатывание конифг сервиса
const ConfigServiceMock: IConfigService = {
	get: jest.fn(),
};
 
// поднимаем работу репозитория по работе с призмой
const UsersRepositoryMock: IUsersRepository = {
	create: jest.fn(),
	find: jest.fn(),
};
 
// контейнер с зависимостями
const container = new Container();
let configService: IConfigService;
let usersRepository: IUsersRepository;
let usersService: IUserService;
 
// выполнится перед всеми функциями
// тут мы биндим моки сервисов
beforeAll(() => {
	container.bind<IUserService>(TYPES.UserService).to(UserService);
	// и уже сюда биндимся не к модулям, а к мокам указанным выше
	container.bind<IConfigService>(TYPES.ConfigService).toConstantValue(ConfigServiceMock);
	container.bind<IUsersRepository>(TYPES.UsersRepository).toConstantValue(UsersRepositoryMock);
 
	// получаем инстансы этих привязок
	configService = container.get<IConfigService>(TYPES.ConfigService);
	usersRepository = container.get<IUsersRepository>(TYPES.UsersRepository);
	usersService = container.get<IUserService>(TYPES.UserService);
});
 
let createdUser: UserModel | null;
 
// содержит описание тестирования
describe('User Service', () => {
	// отдельный тест
	it('createUser', async () => {
		// тут мы говорим, что функция get конфиг сервиса будет возвращать '1'
		configService.get = jest.fn().mockReturnValueOnce('1');
 
		// и теперь, когда что-то дёрнет usersRepository.create(), у нас сработает данный мок
		usersRepository.create = jest.fn().mockImplementationOnce(
			// возврат готового объекта пользователя
			(user: User): UserModel => ({
				name: user.name,
				email: user.email,
				password: user.password,
				id: 1,
			}),
		);
 
		// создаём нового пользователя
		createdUser = await usersService.createUser({
			name: 'Olek',
			email: 'olek@yandex.ru',
			password: 'olekkk',
		});
 
		// далее проверяем полученные значения
		// id пользователя должен быть = 1
		expect(createdUser?.id).toEqual(1);
		// пароль должен храниться в зашифрованном виде и не должен быть равен 1
		expect(createdUser?.password).not.toEqual('1');
	});
});

И отдельно про Mock: для того, чтобы UserService заработал, нужно чтобы в него вложили любой модуль, который удовлетворяет интерфейсу

И этим любым модулем у нас выступает Mock сервис

И после запуска юнит-тестирования мы получаем примерно такой результат, если всё хорошо:

Дальше попробуем допустить ошибку и скажем, что мы ждём от сервера получения пароля не в зашифрованном виде. И, конечно, тут мы получим ошибку.

expect(createdUser?.password).toEqual('1');

094 Упражнение - Новые unit тесты

Допишем три новых теста it() для тестирования валидации данных для поиска пользователя

src > users > users.service.spec.ts

 
/// CODE ...
 
// содержит описание тестирования
describe('User Service', () => {
	// отдельный тест
	it('createUser', async () => {
		// тут мы говорим, что функция get конфиг сервиса будет возвращать '1'
		configService.get = jest.fn().mockReturnValueOnce('1');
 
		// и теперь, когда что-то дёрнет usersRepository.create(), у нас сработает данный мок
		usersRepository.create = jest.fn().mockImplementationOnce(
			// возврат готового объекта пользователя
			(user: User): UserModel => ({
				name: user.name,
				email: user.email,
				password: user.password,
				id: 1,
			}),
		);
 
		// создаём нового пользователя
		createdUser = await usersService.createUser({
			name: 'Olek',
			email: 'olek@yandex.ru',
			password: 'olekkk',
		});
 
		// далее проверяем полученные значения
		// id пользователя должен быть = 1
		expect(createdUser?.id).toEqual(1);
		// пароль должен храниться в зашифрованном виде и не должен быть равен 1
		expect(createdUser?.password).not.toEqual('1');
	});
 
	// проверка успешной валидации
	it('validate - success', async () => {
		// функция find() возвращает нам пользователя один раз
		usersRepository.find = jest.fn().mockReturnValueOnce(createdUser);
 
		// проверяем, существует ли такой пользователь
		const res = await usersService.validateUser({
			email: 'olek@yandex.ru',
			password: 'olekkk',
		});
 
		// res должен провалидировать значение и вернуть true
		expect(res).toBeTruthy();
	});
 
	// тест успешен, если ответ на пароль - false
	it('validate - wrong password', async () => {
		// функция find() возвращает нам пользователя один раз
		usersRepository.find = jest.fn().mockReturnValueOnce(createdUser);
 
		// вводим неправильный пароль
		const res = await usersService.validateUser({
			email: 'olek@yandex.ru',
			password: 'sadfa',
		});
 
		// если пароль неверен, то тест пройден
		expect(res).toBeFalsy();
	});
 
	// тест успешен, если ответ на почту - false
	it('validate - wrong email', async () => {
		// функция find() возвращает нам null - такого пользователя нет
		usersRepository.find = jest.fn().mockReturnValueOnce(null);
 
		// вводим неправильную почту
		const res = await usersService.validateUser({
			email: 'olekdex.ru',
			password: 'olekkk',
		});
 
		// если почта неверна, то тест пройден
		expect(res).toBeFalsy();
	});
});

И мы видим такой результат

095 E2e тесты

Установим модуль супертеста, чтобы запустить систему и провести e2e тестирование

npm i -D supertest @types/supertest

Добавим отдельную команду для запуска джестом отдельного конфига для тестирования

package.json

"scripts": {
	"test": "jest",
	"test:e2e": "jest --config jest.e2e.config.ts",
},

И тут представлен сам конфиг для e2e тестов

`jest.e2e.config.ts

import type { Config } from '@jest/types';
 
const config: Config.InitialOptions = {
	verbose: true,
	preset: 'ts-jest',
	// дирректория, где будут искаться тесты
	rootDir: './tests',
	// паттерн, по которому ищутся тесты
	testRegex: '.e2e-spec.ts$',
};
 
export default config;

Так же нам нужен метод, который сможет закрыть канал сервера, чтобы завершить ивент луп

app.ts

public close(): void {
	this.server.close();
}

Так же нам нужно указать, где происходит запуск приложения (нужна инициализация приложения для начала тестирования)

main.ts

async function bootstrap(): Promise<IBootstrapReturn> {
	const appContainer = new Container();
	appContainer.load(appBindings);
	const app = appContainer.get<App>(TYPES.Application);
 
	// асинхронизируем инициализацию приложения
	await app.init();
 
	return { appContainer, app };
}
 
// и теперь тут мы не можем указать { appContainer, app }, так как это промис
export const boot = bootstrap();

Ну и само тестирование. Оно пройдётся по приложению так же как и юнит-тест.

tests > users.e2e-spec.ts

import { App } from '../src/app';
import { boot } from '../src/main';
import request from 'supertest';
 
// получаем само приложение
let application: App;
 
// перед всеми тестами
beforeAll(async () => {
	// получаем инициализацию приложения
	const { app } = await boot;
	// получаем приложение из инициализированного приложения
	application = app;
});
 
describe('E2e', () => {
	it('register - error', async () => {
		// передаём в супертест инстанс конкретно express приложения
		// дальше мы отправляем запрос по роуту post()
		// и через send() отправляем данные по роуту
		const res = await request(application.app)
			.post('/users/register')
			.send({ email: 'a@yan.ru', password: 1 });
 
		// должен выскочить код 422
		expect(res.statusCode).toBe(422);
	});
});
 
// после всех тестов
afterAll(() => {
	// нам нужно закрыть приложение и все открытые подключения, чтобы тест завершился
	application.close();
});
 

Результат:

096 Упражнение - Дописываем e2e тесты

И далее тут реализуем тесты на валидацию выполняемых функций приложения

tests > users.e2e-spec.ts

import { App } from '../src/app';
import { boot } from '../src/main';
import request from 'supertest';
 
// получаем само приложение
let application: App;
 
// перед всеми тестами
beforeAll(async () => {
	// получаем инициализацию приложения
	const { app } = await boot;
	// получаем приложение из инициализированного приложения
	application = app;
});
 
describe('E2e', () => {
	// при регистрации неверного пользователя должна выскочить ошибка
	it('register - error', async () => {
		// передаём в супертест инстанс конкретно express приложения
		// дальше мы отправляем запрос по роуту post()
		// и через send() отправляем данные по роуту
		const res = await request(application.app)
			.post('/users/register')
			.send({ email: 'a@yan.ru', password: 1 });
 
		// должен выскочить код 422
		expect(res.statusCode).toBe(422);
	});
 
	// при логине существующего пользователя тест должен пройти
	it('login - success', async () => {
		const res = await request(application.app)
			.post('/users/login')
			.send({ email: 'mail@mail.ru', password: '12asdasd' });
 
		expect(res.body.jwt).not.toBeUndefined();
	});
 
	// при неверном логине пользователя тест должен пройти
	it('login - error', async () => {
		const res = await request(application.app)
			.post('/users/login')
			.send({ email: 'mail@mail.ru', password: '1' });
 
		// система должна вернуть при неверном логине код 401
		expect(res.statusCode).toBe(401);
	});
 
	// проводим тест на успешное получение информации о пользователе
	it('Info - success', async () => {
		// сначала мы успешно логинимся
		const login = await request(application.app)
			.post('/users/login')
			.send({ email: 'mail@mail.ru', password: '12asdasd' });
 
		// кидаем запрос на получение информации по пользователю
		const res = await request(application.app)
			.get('/users/login')
			.set('Authorization', `Bearer ${login.body.jwt}`);
 
		// и нам должна вернуться почта человека, который вошёл в систему - тогда тест будет пройден
		expect(res.body.email).toBe('mail@mail.ru');
	});
 
	// проводим тест на неудачение получение информации о пользователе, когда он не залогинился
	it('Info - error', async () => {
		// кидаем запрос на получение информации по пользователю, но с невалидным Bearer
		const res = await request(application.app).get('/users/login').set('Authorization', `Bearer 1`);
 
		// если пользователь не авторизован, то тест пройден
		expect(res.statusCode).toBe(401);
	});
});
 
// после всех тестов
afterAll(() => {
	// нам нужно закрыть приложение и все открытые подключения, чтобы тест завершился
	application.close();
});

Так же можно немного подправить конфиг и убрать из него статичную папку хранения теста

jest.e2e.config

import type { Config } from '@jest/types';
 
const config: Config.InitialOptions = {
	verbose: true,
	preset: 'ts-jest',
	testRegex: '.e2e-spec.ts$',
};
 
export default config;

Флаг --coverage позволяет просмотреть уровень покрытия тестами нашего приложения

package.json

"scripts": {
	"test:e2e": "jest --config jest.e2e.config.ts --coverage",
},

И теперь вместе с выполнением тестов можно увидеть насколько оно покрыто тестами. Самый правый столбик показывает, сколько строк не покрыто тестами - это наша непокрытая функциональность. Гнаться за 100% покрытие приложения тестами - бессмысленно. Это приведёт к большому количеству бесполезных тестов.