[NestJS+Jest] NestJS에 CI 환경 구축
139429 단어 GitHub ActionsCIJestNestJStech
개요
학습회용 NestJS Harson 자료를 만드는 과정에서 여러 가지 자료를 정리해 봤습니다.
CI란 무엇입니까?
CI 지속 포인트
개발자는 자신의 코드를 정기적으로 중앙 창고에 통합한 후 자동화된 구축과 테스트 DevOps 소프트웨어 개발 방법을 실시한다
전제 조건
환경 구축, 사용 기술
Node.js v14.15.4
TypeORM ^8.0.3
NestJS
효율적이고 신축 가능한 노드.js 서버 측면 응용 프로그램 구축에 사용되는 프레임워크
MySQL 8.0
Jest
사전 준비
docker --version
Docker version 20.10.13, build a224086
이번 우승 골문.
작업 디렉토리에 NestJS 프로젝트 만들기
/Desktop/Practice
npm i -g @nestjs/cli
nest new meetUpDev
# パッケージ管理はNPMで行います
? Which package manager would you ❤️ to use?
🚀 Successfully created project meet-up-dev
👉 Get started with the following commands:
$ cd meet-up-dev
$ npm run start
최종 디렉토리 구성
├── ormconfig.test.ts
├── ormconfig.ts
├── package-lock.json
├── package.json
├── src
│ ├── config
│ │ └── configuration.ts
│ ├── controllers
│ │ ├── app.controller.ts
│ │ └── users.controller.ts
│ ├── database
│ │ └── migrations
│ ├── databases
│ │ └── migrations
│ │ └── 1648319214540-user.ts
│ ├── demo.http
│ ├── dto
│ │ └── create-user.dto.ts
│ ├── entities
│ │ └── user.entity.ts
│ ├── main.ts
│ ├── modules
│ │ ├── app.module.ts
│ │ └── users.module.ts
│ └── services
│ ├── app.service.ts
│ └── users.service.ts
├── test
│ ├── jest-e2e.json
│ └── user.e2e-spec.ts
├── tsconfig.build.json
├── tsconfig.json
└── unit-test.yml
원격 웨어하우스 만들기
필요하면 방금 만든 원격 창고를 누르십시오
NestJS 프로젝트 시작
/Desktop/Practice/meet-up-dev
# パッケージを node_modules にインストール
npm i
# コードの変更を検知する
npm run start:dev
[2:39:33 PM] Found 0 errors. Watching for file changes.
# 起動できました!
[Nest] 10374 - 03/26/2022, 2:39:34 PM LOG [NestFactory] Starting Nest application...
[Nest] 10374 - 03/26/2022, 2:39:34 PM LOG [InstanceLoader] AppModule dependencies initialized +35ms
[Nest] 10374 - 03/26/2022, 2:39:34 PM LOG [RoutesResolver] AppController {/}: +11ms
[Nest] 10374 - 03/26/2022, 2:39:34 PM LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 10374 - 03/26/2022, 2:39:34 PM LOG [NestApplication] Nest application successfully started +1ms
끝점으로 GET 요청 보내기
curl -X GET --location "http://localhost:3000"
# Hello World!と表示されればOKです
Hello World!의 반응 메커니즘 해설
응용 프로그램의 입구 파일main입니다.ts
핵심 함수인 NestFactory를 사용하여 Nest 응용 인스턴스의 응용 프로그램 포털 파일을 생성합니다.
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// モジュールからアプリケーションインスタンスを生成
const app = await NestFactory.create(AppModule);
// http://localhost:3000 で起動
await app.listen(3000);
}
bootstrap();
응용 프로그램의 루트 모듈입니다.app.module.ts
응용 프로그램의 루트 모듈입니다.
/src/modules/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from '../controllers/app.controller';
import { AppService } from '../services/app.service';
@Module({
// Moduleで使用するProviderをエクスポートしている他のModule
imports: [],
// このモジュールで定義されている、インスタンス化する必要のあるコントローラのセット
controllers: [AppController],
// モジュール全体で共有される可能性があるプロバイダ
providers: [AppService],
})
export class AppModule {}
단일 컨트롤러
고객 측의 요청 정보를 받아들여 적절한 서비스(루트)에 처리를 분배하다
src/controllers/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from '../services/app.service';
// オプションでルートパスのプレフィックスを定義
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
단일 접근 방식의 기본 서비스
컨트롤러가 내린 명령을 받아 상업 논리적으로 처리하다
src/services/app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
MySQL 컨테이너 구축
meet-up-dev/docker-compose.yml
# docker-composeで使用するバージョンを定義
version: '3'
# アプリケーションを動かすための各要素
services:
# サービス名
db:
# 使用するDockerImage
image: mysql:8.
# Dockerの公式MySQLの文字コードをutf8mb4にする
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
# コンテナの名前
container_name: meetup_db_container
# マウントする設定ファイルのパス
volumes:
- mysql-data-volume:/var/lib/mysql
# ポート番号
ports:
- "3306:3306"
environment:
TZ: 'Asia/Tokyo'
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: meetup
MYSQL_USER: app
MYSQL_PASSWORD: secret
# データの永続化
volumes:
mysql-data-volume:
부팅 컨테이너
docker compose up -d --build
docker compose ps
NAME COMMAND SERVICE STATUS PORTS
meetup_db_container "docker-entrypoint.s…" db running 0.0.0.0:3306->3306/tcp
# コンテナに入ってログインできるか確認しておきましょう
docker-compose exec db bash
root@6481e016e7a8:/# mysql -u root -p
# password
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
TypeORM에 필요한 설정
설치에 필요한 종속성
npm install --save @nestjs/typeorm [email protected] mysql2
npm i --save @nestjs/config
구성 파일 정의
src/config/configuration.ts
export default () => ({
// Node実行時のプロセスから値を取得します
nodeEnv: process.env.NODE_ENV || 'development',
server: {
port: parseInt(process.env.PORT) || 3000,
hostName: process.env.hostname || 'localhost:3000',
},
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 3306,
user: process.env.DB_USERNAME || 'root',
pass: process.env.DB_PASSWORD || 'password',
name: process.env.DB_NAME || 'meetup',
},
});
import Typorm Module
src/modules/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from '../controllers/app.controller';
import { AppService } from '../services/app.service';
import { join } from 'path';
+ import { ConfigModule, ConfigService } from '@nestjs/config';
+ import { TypeOrmModule } from '@nestjs/typeorm';
+ import configuration from '../config/configuration';
@Module({
// Moduleで使用するProviderをエクスポートしている他のModule
imports: [
+ ConfigModule.forRoot({
+ // 他のモジュールでも使用する
+ isGlobal: true,
+ // 設定ファイルを読み込む
+ load: [configuration],
+ }),
+ // TypeORMを構成する
+ TypeOrmModule.forRootAsync({
+ imports: [ConfigModule],
+ useFactory: (configService: ConfigService) => ({
+ type: 'mysql',
+ host: configService.get('database.host'),
+ port: Number(configService.get('database.port')),
+ username: configService.get('database.user'),
+ password: configService.get('database.pass'),
+ database: configService.get('database.name'),
+ entities: [join(__dirname, '../entities/*.entity.{ts,js}')],
+ // アプリケーション実行時にEntityをデータベースに同期しない https://stackoverflow.com/questions/65222981/typeorm-synchronize-in-production
+ synchronize: false,
+ logging: configService.get('nodeEnv') === 'development',
+ extra: {},
+ }),
+ inject: [ConfigService],
+ }),
+ ],
// このモジュールで定義されている、インスタンス化する必要のあるコントローラのセット
controllers: [AppController],
// モジュール全体で共有される可能性があるプロバイダ
providers: [AppService],
})
export class AppModule {}
간단한 CRUD 기능 구현
CRUD Create(등록), 읽기(참조), Update(업데이트), Delete(삭제) 기능의 요약 표현
nest g resource
? What name would you like to use for this resource (plural, e.g., "users")? users
# 今回はREST APIを構築します
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
# 必要な雛形が生成される
ls src/users
dto users.controller.spec.ts users.module.ts users.service.ts
entities users.controller.ts users.service.spec.ts
Enity 클래스 정의
src/users/entities/user.entity.ts
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
PrimaryGeneratedColumn,
Timestamp,
UpdateDateColumn,
} from 'typeorm';
@Entity('users')
export class User {
// 主キーを定義
@PrimaryGeneratedColumn({
name: 'id',
unsigned: true,
type: 'bigint',
comment: 'ユーザーID',
})
readonly id: number;
@Column({
type: 'varchar',
length: 255,
comment: 'ユーザー名',
})
name: string;
@Column({
type: 'varchar',
length: 255,
comment: 'メールアドレス',
unique: true,
})
email: string;
@Column({
type: 'varchar',
length: 255,
comment: 'パスワード',
})
password: string;
@CreateDateColumn({ comment: '登録日時' })
readonly ins_ts?: Timestamp;
@UpdateDateColumn({ comment: '最終更新日時' })
readonly upd_ts?: Timestamp;
@DeleteDateColumn({ comment: '削除日時' })
readonly delete_ts?: Timestamp;
constructor(name: string, email: string, password: string) {
this.name = name;
this.email = email;
this.password = password;
}
}
TypeORM의 설정 파일 정의하기
No connection options were found in any orm configuration files.
ormconfig.tsmodule.exports = {
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || '3306',
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'meetup',
// 手動で作成したマイグレーションですべて管理するのでFalse
synchronize: false,
logging: true,
entities: ['src/entities/*.ts'],
migrations: ['src/databases/migrations/*.ts'],
seeds: ['src/databases/seeders/*.seed.{js,ts}'],
factories: ['src/databases/factories/*.factory.{js,ts}'],
cli: {
migrationsDir: 'src/databases/migrations',
entitiesDir: 'src/entities',
seedersDir: 'src/databases/seeders',
factoriesDir: 'src/databases/factories',
},
};
마이그레이션 파일 생성
npm run build
npx ts-node ./node_modules/.bin/typeorm migration:generate --name user
Migration /Desktop/Practice/meet-up-dev/src/databases/migrations/1648319214540-user.ts has been generated successfully.
생성된 마이그레이션 파일 실행
# コンパイル
npm run build
npx ts-node ./node_modules/.bin/typeorm migration:run
query: START TRANSACTION
query: CREATE TABLE `users` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ユーザーID', `name` varchar(255) NOT NULL COMMENT 'ユーザー名', `email` varchar(255) NOT NULL COMMENT 'メールアドレス', `password` varchar(255) NOT NULL COMMENT 'パスワード', `ins_ts` datetime(6) NOT NULL COMMENT '登録日時' DEFAULT CURRENT_TIMESTAMP(6), `upd_ts` datetime(6) NOT NULL COMMENT '最終更新日時' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `delete_ts` datetime(6) NULL COMMENT '削除日時', UNIQUE INDEX `IDX_97672ac88f789774dd47f7c8be` (`email`), PRIMARY KEY (`id`)) ENGINE=InnoDB
query: INSERT INTO `meetup`.`migrations`(`timestamp`, `name`) VALUES (?, ?) -- PARAMETERS: [1648319214540,"user1648319214540"]
Migration user1648319214540 has been executed successfully.
query: COMMIT
users
양식이 작성되었는지 확인show tables;
+------------------+
| Tables_in_meetup |
+------------------+
| migrations |
| users |
+------------------+
2 rows in set (0.00 sec)
Module 클래스 정의
src/modules/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from '../services/users.service';
import { UsersController } from '../controllers/users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../entities/user.entity';
@Module({
// 現在のスコープに登録されているリポジトリを定義する
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Dto 클래스에서 유효성 정의하기
설치에 필요한 종속성
npm i --save class-validator class-transformer
src/dto/create-user.dto.ts
import {
IsEmail,
IsNotEmpty,
IsOptional,
Matches,
MaxLength,
} from 'class-validator';
import { PartialType } from '@nestjs/mapped-types';
export class CreateUserDto {
@IsNotEmpty({ message: '名前は必ず入力してください' })
@MaxLength(255, {
message: '名前は255文字以内で入力してください',
})
name: string;
@IsNotEmpty({ message: 'Emailは必ず入力してください' })
@MaxLength(255, {
message: 'Emailは255文字以内で入力してください',
})
@IsEmail({ message: '正しいEmail形式で入力してください' })
email: string;
@IsNotEmpty({ message: `パスワードは必ず入力してください` })
@Matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,25}$/, {
message: `パスワードは大文字小文字を含む8文字以上25文字以内で設定してください`,
})
password: string;
}
export class UpdateUserDto extends PartialType(CreateUserDto) {
// 値がNULLならバリデーションを無視する
@IsOptional()
@Matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,25}$/, {
message: `パスワードは大文字小文字を含む8文字以上25文字以内で設定してください`,
})
password: string;
}
Controller 클래스 정의
src/controllers/users.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
} from '@nestjs/common';
import { UsersService } from '../services/users.service';
import { CreateUserDto, UpdateUserDto } from '../dto/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
}
서비스 클래스 정의
npm i bcrypt
npm i -D @types/bcrypt
src/services/users.service.ts
import {
BadRequestException,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { CreateUserDto, UpdateUserDto } from '../dto/create-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../entities/user.entity';
import { Not, Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(
// EntityをDIすることでRepositoryを経由してDBに問い合わせを行う
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
/**
* 登録
* @param createUserDto
*/
async create(createUserDto: CreateUserDto): Promise<{ message: string }> {
if (
(await this.usersRepository.find({ email: createUserDto.email })).length
) {
throw new BadRequestException('既に登録ずみのメールアドレスです');
}
await this.usersRepository
.save({
name: createUserDto.name,
email: createUserDto.email,
password: await bcrypt.hash(createUserDto.password, 10),
})
.catch((e) => {
throw new InternalServerErrorException(
`[${e.message}]ユーザー登録に失敗しました。`,
);
});
return { message: 'ユーザーの登録に成功しました' };
}
/**
* ユーザー一覧を取得
*/
async findAll(): Promise<User[]> {
return this.usersRepository.find();
}
/**
* @description IDに該当するユーザーを取得
* @param id
*/
async findOne(id: number): Promise<User> {
return this.usersRepository.findOneOrFail(id);
}
/**
* 更新
* @param id
* @param updateUserDto
*/
async update(
id: number,
updateUserDto: UpdateUserDto,
): Promise<{ message: string }> {
if (
(
await this.usersRepository.find({
email: updateUserDto.email,
id: Not(id),
})
).length
) {
throw new BadRequestException('既に登録ずみのメールアドレスです');
}
const baseUser = await this.usersRepository.findOneOrFail(id);
await this.usersRepository
.update(id, {
name: updateUserDto.name,
email: updateUserDto.email,
password: updateUserDto.password
? await bcrypt.hash(updateUserDto.password, 10)
: baseUser.password,
})
.catch((e) => {
throw new InternalServerErrorException(
`[${e.message}]ユーザーID「${id}」の更新に失敗しました。`,
);
});
return { message: `ユーザーID「${id}」の更新に成功しました。` };
}
/**
* 削除
* @param id
*/
async remove(id: number) {
await this.usersRepository.delete(id).catch((e) => {
throw new InternalServerErrorException(
`[${e.message}]ユーザーID「${id}」の削除に失敗しました。`,
);
});
return { message: `ユーザーID「${id}」の削除に成功しました。` };
}
}
생성된 엔드포인트에 요청
# 登録
curl -X POST -H "Content-Type:application/json" localhost:3000/users -d '{"name": "ほげ", "email": "[email protected]", "password": "Password1234"}'
curl -X POST -H "Content-Type:application/json" localhost:3000/users -d '{"name": "ほげ2", "email": "[email protected]", "password": "Password1234"}'
# 更新
curl -X PATHC -H "Content-Type:application/json" localhost:3000/users/2 -d '{"name": "ふが", "email": "[email protected]", "password": "Password1234"}'
# 一覧
curl -X GET --location "http://localhost:3000/users"
# 詳細
curl -X GET --location "http://localhost:3000/users/2"
# 削除
curl -X DELETE --location "http://localhost:3000/users/2"
E2E 테스트의 실현
E2E(End to End) Test는 User Interface Test라고도 하며 시스템 전체를 통해 테스트됩니다.
웹 서비스의 경우 사용자와 마찬가지로 브라우저를 조작하여 동작이 예상과 일치하는지 확인합니다.
테스트 TypeORM 설정 정의
자동 마이그레이션 기능이 없으므로 테스트할 때만 Enity를 데이터베이스와 동기화할 수 있습니다.
// アプリケーション実行時にEntityをデータベースに同期する
synchronize: true,
ormconfig.test.tsmodule.exports = {
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || '3306',
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'meetup',
// アプリケーション実行時にEntityをデータベースに同期する
synchronize: true,
// 実行されるSQLをログとして吐く
logging: true,
migrationsRun: true,
dropSchema: true,
entities: ['src/entities/*.ts'],
migrations: ['src/databases/migrations/*.ts'],
seeds: ['src/databases/seeders/*.seed.{js,ts}'],
factories: ['src/databases/factories/*.factory.{js,ts}'],
cli: {
migrationsDir: 'src/databases/migrations',
entitiesDir: 'src/entities',
seedersDir: 'src/databases/seeders',
factoriesDir: 'src/databases/factories',
},
};
테스트 코드 설치
이번에는 로그인 기능에 초점을 맞추어 테스트 코드를 씁니다.
유효성 검사를 위한 패키지 설치
npm i randomstring
npm i typeorm-seeding
test/user.e2e-spec.ts
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../src/entities/user.entity';
import { ConfigModule } from '@nestjs/config';
import { AppModule } from '../src/modules/app.module';
import { UsersController } from '../src/controllers/users.controller';
import { CreateUserDto } from '../src/dto/create-user.dto';
import * as request from 'supertest';
import * as randomstring from 'randomstring';
import { UsersService } from '../src/services/users.service';
import { useRefreshDatabase } from 'typeorm-seeding';
describe('UserController(E2E)', () => {
// モジュール設定準備します
let app: INestApplication;
// テスト実行時に毎回実行
beforeEach(async () => {
// DBに接続&内部のデータをリフレッシュ
await useRefreshDatabase();
});
// テスト前に1回だけ実行
beforeAll(async () => {
const moduleFixure: TestingModule = await Test.createTestingModule({
imports: [
// EntityをDIする
TypeOrmModule.forFeature([User]),
// テスト専用のTypeORM設定を読み込む
ConfigModule.forRoot({ envFilePath: 'ormconfig.test.ts' }),
AppModule,
],
controllers: [UsersController],
providers: [UsersService],
}).compile();
// 実行環境をインスタンス化
app = moduleFixure.createNestApplication();
// ValidationPipe有効化
app.useGlobalPipes(new ValidationPipe());
// モジュールの初期化
await app.init();
});
// テスト後に1回だけ実行
afterAll(async () => {
// テスト終了
await app.close();
});
describe('ユーザー登録テスト', () => {
// API の実行に成功する
it('OK /users (POST)', async () => {
const body: CreateUserDto = {
name: 'Test',
email: '[email protected]',
password: 'Password1234',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(201);
});
it('NG /users 重複判定 (POST)', async () => {
const body: CreateUserDto = {
name: 'Test',
email: '[email protected]',
password: 'Password1234',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
const DuplicateBody: CreateUserDto = {
name: 'Test',
email: '[email protected]',
password: 'Password1234',
};
const ErrorRes = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(ErrorRes.status).toEqual(400);
});
it('NG /users name255文字以上(POST)', async () => {
const body: CreateUserDto = {
name: randomstring.generate(256),
email: '[email protected]',
password: 'Password1234',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(400);
});
it('NG /users nameNull(POST)', async () => {
const body: CreateUserDto = {
name: null,
email: '[email protected]',
password: 'Password1234',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(400);
});
it('NG /users email256文字(POST)', async () => {
const body: CreateUserDto = {
name: 'test',
email: `${randomstring.generate(256)}@example.com`,
password: 'Password1234',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(400);
});
it('NG /users email形式エラー(POST)', async () => {
const body: CreateUserDto = {
name: 'test',
email: `${randomstring.generate(10)}`,
password: 'Password1234',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(400);
});
it('NG /users emailNull(POST)', async () => {
const body: CreateUserDto = {
name: 'test',
email: null,
password: 'Password1234',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(400);
});
it('NG /users passwordNull(POST)', async () => {
const body: CreateUserDto = {
name: 'test',
email: '[email protected]',
password: null,
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(400);
});
it('NG /users password形式エラー(POST)', async () => {
const body: CreateUserDto = {
name: 'test',
email: '[email protected]',
password: 'pass',
};
const res = await request(app.getHttpServer())
.post('/users')
.set('Accept', 'application/json')
.send(body);
expect(res.status).toEqual(400);
});
});
});
E2E 테스트를 수행하는 스크립트 명령 업데이트
# 現在のプロセスで全てのテストを1つずつ実行
--runInBand
# 全テストが終了した後にJestを強制的に終了
--forceExit
# Jest が何も出力せずに終了するのを防ぐ
--detectOpenHandles
# テストの探索と実行の方法を指定する Jest の設定ファイルのパスを引数に指定
--config
package.json{
"name": "meet-up-dev",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
+ "test:e2e": "jest --runInBand --forceExit --detectOpenHandles --config ./test/jest-e2e.json"
},
E2E 테스트 수행
npm run test:e2e
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 8.53 s
Ran all test suites.
GiitHubAction에서 워크플로우 정의
테스트 Docker File 정의
DockerfileTest
# FROM ベースイメージを指定する
# as build-stage ビルド用のイメージと実行用のイメージを分ける
# -alpine 軽量イメージ
FROM node:14.16.1-alpine as build-stage
# 作業ディレクトリを作成
WORKDIR /work
COPY . /work/
RUN npm install
# コマンドを実行する
CMD ["npm","run","test:e2e"]
테스트 실행을 위한 MySQL 컨테이너 구축
unit-test.yml
# docker-composeで使用するバージョン
version: '3'
# アプリケーションを動かすための各要素
services:
# コンテナ名
app:
# ComposeFileを実行し、ビルドされるときのpath
build:
# docker buildコマンドを実行した場所
context: "."
# Dockerfileのある場所
dockerfile: "DockerfileTest"
# コンテナ名
container_name: github-actions-api-test
# ポート番号
ports:
- '3000:3000'
# 環境変数
environment:
PORT: 3000
TZ: 'Asia/Tokyo'
DB_HOST: 'testdb'
DB_PORT: '3306'
DB_USERNAME: 'root'
DB_PASSWORD: 'password'
DB_NAME: 'meetup'
# サービス間の依存関係
depends_on:
- testdb
testdb:
image: mysql:8.0
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
container_name: db_container_e2e_test
ports:
- 3306:3306
environment:
TZ: 'Asia/Tokyo'
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: meetup
MYSQL_USER: app
MYSQL_PASSWORD: secret
워크플로우 정의
run_test.yml
# ワークフローをトリガーするGitHubイベントの名前
on:
# mainブランチにプッシュすると実行する
push:
branches:
- main
# ジョブ定義
jobs:
run-test:
name: Run Test
# ubuntu環境で動作
runs-on: ubuntu-latest
# アクションを定義
steps:
- name: checkout pushed commit
# ソースコードのチェックアウト
uses: actions/checkout@v2
with:
# PRのHEADブランチを使う
ref: ${{ github.event.pull_request.head.sha }}
# E2E テストを Docker Compose で実行する
- name: run test on docker-compose
run: |
docker-compose -f ./unit-test.yml build
docker-compose -f ./unit-test.yml up --abort-on-container-exit
working-directory: ./
main 지점으로 미루기
E2E 테스트를 정의하는 워크플로우 실행 여부를 확인할 수 있습니다.
Reference
이 문제에 관하여([NestJS+Jest] NestJS에 CI 환경 구축), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/naonao70/articles/80511f2404df9d텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)