概要
こちらの記事はイベント登壇時のデモプロジェクトをハンズオンで構築できるように整理したものになります
プロジェクトの作成
任意の作業ディレクトリでプロジェクトをセットアップします
パッケージ管理システムはnpm
を利用します
npm i -g @nestjs/cli
nest new meetup_demo
⚡ We will scaffold your app in a few seconds..
? Which package manager would you ❤️ to use? npm
✔ Installation in progress... ☕
🚀 Successfully created project meetup_demo
👉 Get started with the following commands:
$ cd meetup_demo
$ npm run start
プロジェクトを起動します
プロジェクトの起動に成功したら任意のレポジトリを作成してコミットを行なってください
cd meetup_demo
# 差分を自動検知する watchモードで起動します
npm run start:dev
[3:44:27 AM] Starting compilation in watch mode...
[3:44:31 AM] Found 0 errors. Watching for file changes.
[Nest] 6003 - 04/20/2022, 3:44:32 AM LOG [NestFactory] Starting Nest application...
[Nest] 6003 - 04/20/2022, 3:44:32 AM LOG [InstanceLoader] AppModule dependencies initialized +51ms
[Nest] 6003 - 04/20/2022, 3:44:32 AM LOG [RoutesResolver] AppController {/}: +67ms
[Nest] 6003 - 04/20/2022, 3:44:32 AM LOG [RouterExplorer] Mapped {/, GET} route +5ms
[Nest] 6003 - 04/20/2022, 3:44:32 AM LOG [NestApplication] Nest application successfully started +4ms
Dockerファイルを作成します
# ベースイメージ
FROM node:14.16.1-alpine as build-develop
# 作業ディレクトリ
WORKDIR /work
# プロジェクト配下を/work配下に配置
COPY . /work/
# プログレスバーを非表示にして高速化
RUN npm install --no-progress
# eslintを実行
RUN npm run lint
# 型定義ファイルを作成
RUN npm run build
FROM node:14.16.1-alpine as build-stage
# localeを日本語に
ENV LANG C.UTF-8
ENV TZ Asia/Tokyo
WORKDIR /app
COPY ./package.json ./package-lock.json ./
# --cache /tmp/empty-cache 一時的なキャッシュを利用
RUN npm install --production --no-progress --cache /tmp/empty-cache && rm -rf /tmp/empty-cache
COPY --from=build-stage /work/dist ./dist
# https://github.com/krallin/tini
# PID 1対策
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
USER node
# ホストマシン上にポートを露出(expose)しますが、公開はしません
EXPOSE 3000
ENV NODE_ENV prod
CMD ["node", "dist/src/main"]
services:
db:
# 使用イメージ
image: mysql:8.0
# Dockerの公式MySQLの文字コードをutf8mb4にする
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
container_name: meetup_db_container
# ホスト(mysql-data-volume)をコンテナ(/var/lib/mysql)にマウント
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:
コンテナを起動します
MySQLコンテナが起動できていることを確認したら一旦コミットしましょう
docker-compose up -d ✔ 04:40:34
Creating meetup_db_container ... done
docker-compose ps ✔ 04:40:37
Name Command State Ports
------------------------------------------------------------------------------------------------
meetup_db_container docker-entrypoint.sh mysql ... Up 0.0.0.0:3306->3306/tcp, 33060/tcp
環境変数を定義します
依存関係をインストールします
npm i --save @nestjs/config
mkdir src/config && vi src/config/configuration.ts
環境変数を定義する設定ファイルを作成します
export default () => ({
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',
},
});
TyprORMの構成ファイルを定義していきます
依存関係をインストールします
npm install --save @nestjs/typeorm typeorm@0.2 mysql2
TypeORMの設定ファイルに接続内容を定義します
※ この手順を飛ばすと接続オプションが見つからないためエラーとなります
No connection options were found in any orm configuration files
設定ファイルを定義します
vi ormconfig.ts
module.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: false,
// クエリおよびエラーのロギングを有効にする
logging: true,
entities: ['src/**/**/*.entity{.ts,.js}'],
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',
},
};
ルートモジュールで構成ファイルを読み込みます
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
import { ConfigModule, ConfigService } from '@nestjs/config';
import configuration from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
// 他のモジュールでConfigModuleをインポートせずに利用できるように
isGlobal: true,
// 環境変数を定義したファイルをインポート
load: [configuration],
}),
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}')],
synchronize: false,
logging: configService.get('nodeEnv') === 'development',
extra: {},
}),
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
TypeORMの設定が完了したのでプロジェクトを起動します
TypeOrmModuleの初期化が成功していることを確認できればOKです。
コミットしておきましょう
docker-compose up -d
npm run start:dev
[Nest] 6243 - 04/22/2022, 4:14:07 AM LOG [NestFactory] Starting Nest application...
[Nest] 6243 - 04/22/2022, 4:14:07 AM LOG [InstanceLoader] TypeOrmModule dependencies initialized +38ms
CRUD機能を実装します
CRUD generator
で雛形を作ります
nest g resource cats ✔ 19s 00:11:53
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/cats/cats.controller.spec.ts (556 bytes)
CREATE src/cats/cats.controller.ts (873 bytes)
CREATE src/cats/cats.module.ts (240 bytes)
CREATE src/cats/cats.service.spec.ts (446 bytes)
CREATE src/cats/cats.service.ts (595 bytes)
CREATE src/cats/dto/create-cat.dto.ts (29 bytes)
CREATE src/cats/dto/update-cat.dto.ts (165 bytes)
CREATE src/cats/entities/cat.entity.ts (20 bytes)
UPDATE package.json (2148 bytes)
UPDATE src/app.module.ts (1404 bytes)
Entity
を定義します
-
Entity
とは
データベースのテーブルに対応するクラスのことです
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
Timestamp,
UpdateDateColumn,
} from 'typeorm';
@Entity('cats')
export class Cat {
@PrimaryGeneratedColumn({
name: 'id',
unsigned: true,
type: 'smallint',
comment: 'ID',
})
readonly id: number;
@Column('varchar', { comment: '猫の名前' })
name: string;
@CreateDateColumn({ comment: '登録日時' })
readonly ins_ts?: Timestamp;
@UpdateDateColumn({ comment: '最終更新日時' })
readonly upd_ts?: Timestamp;
constructor(name: string) {
this.name = name;
}
}
マイグレーションファイルを自動で生成します
# npx package.jsonにインストールせず使うのでnpxを利用しています
# ts-node tsファイルを直接実行するので、ts-node を利用します
npx ts-node ./node_modules/.bin/typeorm migration:generate -n cats
# 自動で作成されれば成功です
Migration /Users/habanaoki/Desktop/Practice/meetup_demo/src/databases/migrations/1650814095673-cats.ts has been generated successfully.
マイグレーションファイルを実行します
# 現在の型情報をdist配下にビルドします
npm run build
# マイグレーションを実行します
npx ts-node ./node_modules/.bin/typeorm migration:run
# 成功すればOKです
Migration cats1650814095673 has been executed successfully.
query: COMMIT
テーブルが作成されていることをMySQLコンテナ内で確認します
# DBコンテナに入ります
docker-compose exec db bash ✔ 00:38:50
# ログインします
root@e6d879736822:/# mysql -u root -p
# password を入力します
Enter password:
# meetup を利用します
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| meetup |
| mysql |
| performance_schema |
| sys |
+--------------------+
mysql> use meetup;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
# 先ほど定義したEntityのデータ型通りに作成されていることが確認できたらOKです
mysql> DESC cats;
+--------+-------------------+------+-----+----------------------+--------------------------------------------------+
| Field | Type | Null | Key | Default | Extra |
+--------+-------------------+------+-----+----------------------+--------------------------------------------------+
| id | smallint unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| ins_ts | datetime(6) | NO | | CURRENT_TIMESTAMP(6) | DEFAULT_GENERATED |
| upd_ts | datetime(6) | NO | | CURRENT_TIMESTAMP(6) | DEFAULT_GENERATED on update CURRENT_TIMESTAMP(6) |
+--------+-------------------+------+-----+----------------------+--------------------------------------------------+
ここまで確認できましたらコミットしておきましょう
モジュールを実装します
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from './entities/cat.entity';
@Module({
// 特定のEntityのみimportします
imports: [TypeOrmModule.forFeature([Cat])],
// モジュール外でも利用できるようにします
exports: [TypeOrmModule],
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
バリデーションクラスを実装します
依存関係をインストールします
npm install class-validator --save
import { IsNotEmpty, MaxLength } from 'class-validator';
export class CreateCatDto {
@IsNotEmpty({ message: '名前は必須項目です' })
@MaxLength(255, { message: '名前は255文字以内で入力してください' })
name: string;
}
import { PartialType } from '@nestjs/mapped-types';
import { CreateCatDto } from './create-cat.dto';
import { IsNotEmpty, MaxLength } from 'class-validator';
export class UpdateCatDto extends PartialType(CreateCatDto) {
@IsNotEmpty({ message: '名前は必須項目です' })
@MaxLength(255, { message: '名前は255文字以内で入力してください' })
name: string;
}
バリデーションを適用する処理をmain.ts
に追加します
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// クロスオリジンリソース共有を有効にする
app.enableCors();
// バリデーションを自動で適用
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap().catch((err) => {
process.exit(1);
});
コントローラークラスを実装します
想定する戻り値の型と非同期で実行する処理を定義します
現時点ではサービスクラスの戻り値と一致しないのでエラーになります
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';
import { DeleteResult, UpdateResult } from 'typeorm';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto): Promise<Cat> {
return await this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return await this.catsService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<Cat> {
return await this.catsService.findOne(+id);
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() updateCatDto: UpdateCatDto,
): Promise<UpdateResult> {
return await this.catsService.update(+id, updateCatDto);
}
@Delete(':id')
async remove(@Param('id') id: string): Promise<DeleteResult> {
return await this.catsService.remove(+id);
}
}
サービスクラスを実装します
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, Repository, UpdateResult } from 'typeorm';
@Injectable()
export class CatsService {
constructor(@InjectRepository(Cat) private catRepository: Repository<Cat>) {}
/**
* @summary 登録機能
* @param createCatDto
*/
async create(createCatDto: CreateCatDto): Promise<Cat> {
return await this.catRepository
.save({ name: createCatDto.name })
.catch((e) => {
throw new InternalServerErrorException(e.message);
});
}
/**
* @summary 全件取得
*/
async findAll(): Promise<Cat[]> {
return await this.catRepository.find().catch((e) => {
throw new InternalServerErrorException(e.message);
});
}
/**
* @summary 該当ID取得
* @param id
*/
async findOne(id: number): Promise<Cat> {
const cat = await this.catRepository.findOne(id);
if (!cat) {
throw new NotFoundException(
`${id}に一致するデータが見つかりませんでした。`,
);
}
return cat;
}
/**
* @summary 該当ID更新
* @param id
*/
async update(id: number, updateCatDto: UpdateCatDto): Promise<UpdateResult> {
{
return await this.catRepository
.update(id, { name: updateCatDto.name })
.catch((e) => {
throw new InternalServerErrorException(e.message);
});
}
}
/**
* @summary 削除
* @param id
*/
async remove(id: number): Promise<DeleteResult> {
return await this.catRepository.delete(id).catch((e) => {
throw new InternalServerErrorException(e.message);
});
}
}
APIエンドポイントにリクエストを投げてみましょう
# create
curl -X POST --location "http://localhost:3000/cats" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Hoge\"
}"
# findAll
curl -X GET --location "http://localhost:3000/cats" \
-H "Accept: application/json"
# findOne
curl -X GET --location "http://localhost:3000/cats/1" \
-H "Accept: application/json"
# update
curl -X PATCH --location "http://localhost:3000/cats/1" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Hogehoge\"
}"
# delete
curl -X DELETE --location "http://localhost:3000/cats/1" \
-H "Content-Type: application/json"
E2Eテストを実装していきます
E2E(End to End)Testは、User Interface Testとも呼ばれ、システム全体を通してテストをおこないます。
Webサービスの場合は、ユーザと同じようにブラウザを操作し、挙動が期待通りになっているか確認します。
テストで利用するモジュールをインストールします
-
typeorm-seeding
モジュール内にデータベースをリフレッシュするメソッドが入っているので利用します -
randomstring
:ランダムな文字列を生成するために利用します
npm i --save-dev @nestjs/testing randomstring typeorm-seeding
テストの設定を行います
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { tearDownDatabase, useRefreshDatabase } from 'typeorm-seeding';
import { CatsModule } from '../src/cats/cats.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from '../src/cats/entities/cat.entity';
import { AppModule } from '../src/app.module';
import { CatsController } from '../src/cats/cats.controller';
describe('CatsController (e2e)', () => {
let app: INestApplication;
// 毎回実行される処理
beforeEach(async () => {
// DBをリフレッシュ
await useRefreshDatabase();
});
// テスト前に1回だけ実行する処理
beforeAll(async () => {
// テストモジュールにテスト対象モジュールに必要な対象をDIする
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TypeOrmModule.forFeature([Cat]), CatsModule, AppModule],
controllers: [CatsController],
}).compile();
app = moduleFixture.createNestApplication();
// バリデーションを有効にする
app.useGlobalPipes(new ValidationPipe());
await app.init();
// テスト後に1回だけ実行する
afterAll(async () => {
// テスト終了
await app.close();
// DB接続終了
await tearDownDatabase();
});
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
テストを実装していきます
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { tearDownDatabase, useRefreshDatabase } from 'typeorm-seeding';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from '../src/cats/entities/cat.entity';
import { AppModule } from '../src/app.module';
import { CatsController } from '../src/cats/cats.controller';
import { CreateCatDto } from '../src/cats/dto/create-cat.dto';
import { CatsService } from '../src/cats/cats.service';
import * as randomstring from 'randomstring';
describe('CatsController (e2e)', () => {
let app: INestApplication;
// テスト実行時に毎回実行します
beforeEach(async () => {
// DBに接続&内部のデータをリフレッシュ
await useRefreshDatabase();
});
// テスト前に1回だけ実行します
beforeAll(async () => {
// モジュールでDIしている対象を定義します
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TypeOrmModule.forFeature([Cat]), AppModule],
controllers: [CatsController],
providers: [CatsService],
}).compile();
// モジュールからインスタンスを作成します
app = moduleFixture.createNestApplication();
// Auto-validation
app.useGlobalPipes(new ValidationPipe());
// モジュールの初期化
await app.init();
});
// テスト後に1回だけ実行します
afterAll(async () => {
// テスト終了
await app.close();
// DBとの接続を終了する
await tearDownDatabase();
});
async function saveCat(body: CreateCatDto): Promise<request.Response> {
return request(app.getHttpServer())
.post('/cats')
.set('Accept', 'application/json')
.send(body);
}
async function getById(id: number): Promise<request.Response> {
return request(app.getHttpServer()).get(`/cats/${id}`);
}
it('/ create(OK)(POST)', async () => {
const dto: CreateCatDto = { name: 'テスト' };
const res = await saveCat(dto);
expect(res.body.name).toEqual(dto.name);
expect(res.status).toEqual(201);
});
it('/ create(NG)-名前未入力(POST)', async () => {
const dto: CreateCatDto = { name: null };
const res = await saveCat(dto);
expect(res.status).toEqual(400);
expect(res.body.message).toEqual([
'名前は255文字以内で入力してください',
'名前は必須項目です',
]);
});
it('/ create(NG)-名前255文字以上(POST)', async () => {
const dto: CreateCatDto = { name: randomstring.generate({ length: 256 }) };
const res = await saveCat(dto);
expect(res.status).toEqual(400);
expect(res.body.message).toEqual(['名前は255文字以内で入力してください']);
});
it('/ findAll(OK)(GET)', async () => {
const dto: CreateCatDto = { name: randomstring.generate({ length: 10 }) };
await saveCat(dto);
const res = await request(app.getHttpServer()).get('/cats');
expect(res.status).toEqual(200);
const { name } = res.body[0];
expect(name).toEqual(dto.name);
});
it('/ findOne(OK)(GET)', async () => {
const dto: CreateCatDto = { name: randomstring.generate({ length: 10 }) };
const cat = await saveCat(dto);
const res = await getById(cat.body.id);
expect(res.status).toEqual(200);
expect(res.body.name).toEqual(dto.name);
});
it('/ findOne(NG)-ID該当なし(GET)', async () => {
const res = await getById(9999);
expect(res.status).toEqual(404);
});
it('/ update(OK)(PATCH)', async () => {
const body: CreateCatDto = {
name: randomstring.generate({ length: 10 }),
};
const cat = await request(app.getHttpServer())
.post('/cats')
.set('Accept', 'application/json')
.send(body);
await request(app.getHttpServer())
.patch(`/cats/${cat.body.id}`)
.set('Accept', 'application/json')
.send({ name: '更新' });
const data = await request(app.getHttpServer()).get(`/cats/${cat.body.id}`);
expect(data.status).toEqual(200);
expect(data.body.name).toEqual('更新');
});
it('/ remove(OK)(DELETE)', async () => {
const body: CreateCatDto = {
name: randomstring.generate({ length: 10 }),
};
const cat = await request(app.getHttpServer())
.post('/cats')
.set('Accept', 'application/json')
.send(body);
const res = await request(app.getHttpServer()).delete(
`/cats/${cat.body.id}`,
);
expect(res.status).toEqual(200);
const data = await getById(cat.body.id);
expect(data.status).toEqual(404);
});
});
テストを実行してみます
テストが全て通ればOKです
PASS test/cats.e2e-spec.ts (11.379 s)
PASS test/app.e2e-spec.ts
Test Suites: 2 passed, 2 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 12.42 s
テスト用DBコンテナを構築します
ここまででテストの自動化は完了しましたが、毎回手動でテストを実行するのは微妙なのでGitHubActions
で自動的に実行されるようにしていきます
そのため、GitHubActions経由で行うCI用のDBコンテナを構築していきます
FROM node:14.16.1-alpine as build-test-db
# workディレクトリ内にマウントします
WORKDIR /work
COPY . /work/
RUN npm install
CMD ["npm","run","test:e2e"]
# 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
テストを実行するスクリプトを編集します
並列でテストが実行されることで、マイグレーションやデータベースのリフレッシュ処理とテストのタイミングがバッティングしてしまいテストが失敗することがあります。そのため、以下のオプションを追加しておきます
-
--runInBand
:現在のプロセスで全てのテストを1つずつ実行 -
--forceExit
:全テストが終了した後にJestを強制的に終了
"test:e2e": "jest --runInBand --forceExit --config ./test/jest-e2e.json"
GitHubのワークフローを定義します
# ワークフローをトリガーするGitHubイベントの名前
on:
# mainブランチにPRすると実行する
pull_request:
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 ./e2e-test.yml build
docker-compose -f ./e2e-test.yml up --abort-on-container-exit
working-directory: ./
mainブランチにプッシュしてみましょう
自動でテストが実行されれば成功です
まとめ
ここまででテストを書いてGitHubActionsでテストを実行するCI環境を構築できました
いかがでしょうか? テストコードを書くことで自身のコードが最低限動作していることを担保できます。
またリファクタリングのハードルも下げることができるので是非導入してみてはいかがでしょうか??