Nest.js とは、TypeScript で構築されたスケーラブルで効率的なサーバーサイド Node.js フレームワークであり、 Node.js の開発で構造に関するデザインパターンを使うために作成されました。 Angular.js から着想を得て、Express.js をベースにしており、 Express.js ミドルウェアの大部分と互換性を持っています。
このチュートリアルでは、Nest.js を使った RESTful API の開発方法を説明します。 この開発を通じて、Nest.js の基本原則と構成要素を身につけられます。 また、各 API エンドポイントのテストを作成するための推奨アプローチも示します。 終わりに、CircleCI を使ってテストプロセスを自動化する方法も紹介します。
前提条件
このチュートリアルを進めるには、以下のものが必要です。
- Node バージョン
10.24.1
(LTS/Dubnium) - MySQL バージョン
8.0.20
以上 - Nest CLI バージョン
nestjs/cli@8.0.0
以上 - GitHub アカウント
- CircleCI アカウント
- TypeScript の基本知識 (なくても可)
本チュートリアルはさまざまな CI/CD プラットフォームに適用できますが、例として CircleCI を使用しています。 CircleCI アカウントをお持ちでない場合は、こちらから無料アカウントを作成してください。
この記事で開発する RESTful API では、プロダクトを名前、説明、価格付きで登録するエンドポイントをプロビジョニングします。 この API により、1 つのプロダクトを編集、削除、取得するほか、データベースに保存されている全プロダクトのリストを取得します。
このチュートリアルでは、リレーショナルデータベースとして MySQL を利用し、TypeORM と組み合わせて使用します。 ただし、Nest.js はどのデータベースにも対応しているので、お好みに応じて任意のデータベースを選択してかまいません。 データベースと Nest.js に関する詳細はこちらをご覧ください。
Nest.js アプリケーションをセットアップする
以下のコマンドを実行して、新しいアプリケーションを作成します。
nest new nest-starter-testing
上記の nest
コマンドを実行すると、パッケージマネージャーの選択を求められます。 npm
を選択して Enter キーを押すと、Nest.js のインストールが始まります。 これで、nest-starter-testing
フォルダーに新しいプロジェクトが作成され、必要な依存関係すべてがインストールされます。 アプリケーションを実行する前に、npm
を使用して 検証ライブラリをインストールします。このライブラリは、このチュートリアルで後ほど使用します。
npm install class-validator --save
以下のコマンドを使用して、アプリケーションフォルダーに移動しアプリケーションを起動します。
// プロジェクトに移動する
cd nest-starter-testing
// サーバーを起動する
npm run start:dev
これで、デフォルトのポート 3000
でアプリケーションが起動します。 任意のブラウザーで http://localhost:3000
にアクセスし、確認します。
![Nest.js のデフォルトページ]((//images.ctfassets.net/il1yandlcjgk/2NauOYBdtRBWxTbRUerL4v/9c5dd4372f5681c562724d18f6291ffe/2022-04-14-nest-homepage.png){: .zoomable }
Nest.js を構成してデータベースに接続する
TypeORM は、TypeScript アプリケーションと JavaScript アプリケーションに使用される人気の O/R マッパー (ORM) です。 この ORM を Nest.js アプリケーションとスムーズに統合できるように、Nest.js 付属のパッケージを MySQL 用 Node.js ドライバーと一緒にインストールしましょう。 Ctrl + C を押してアプリケーションの実行を停止してから、以下のコマンドを実行します。
npm install --save @nestjs/typeorm typeorm mysql
インストールプロセスが完了したら、TypeOrmModule
をアプリケーションのルートにインポートできるようになります。
TypeScript ルートモジュールを更新する
Nest.js の構成要素はモジュールであり、TypeScript ファイルで @Module
デコレータを使って定義します。 モジュールの役目は、Nest.js にアプリケーション構造の整理用のメタデータを提供することです。 ./src/app.module.ts
にあるルートモジュールが最上位モジュールです。 Nest.js では、大規模なアプリケーションは複数のモジュールに分割することが推奨されています。 こうすることで、アプリケーション構造を保守しやすくなるからです。
データベースとの接続を作成するために、./src/app.module.ts
ファイルを開いて内容を以下のコードに置き換えます。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: DB_USER,
password: DB_PASSWORD,
database: 'test_db',
entities: [join(__dirname, '**', '*.entity.{ts,js}')],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
注: DB_USER
と DB_PASSWORD
は、お使いの認証情報に置き換えてください。
これで、接続オプションを指定して TypeOrmModule
をルート AppModule
にインポートすることで、データベースとの接続を作成できました。 接続オプションでは、データベースの詳細と、エンティティファイルの保存先となるディレクトリを設定しています。 エンティティファイルについては、次のセクションで詳しく説明します。
データベース接続を構成する
このチュートリアル冒頭の前提条件に、MySQL のダウンロードページを載せています。 このデータベースをダウンロードした後で、アプリケーションで使えるように構成する必要があります。
ターミナルで以下のコマンドを実行し、MySQL にログインします。
mysql -u root -p
MySQL のインストール中に設定したパスワードを入力します。 次に、以下を実行します。
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
‘password’ は実際のパスワードに置き換えてください。
このコマンドは、MySQL 用 Node.js ドライバーに任意の認証情報を設定するものです。 以下のコマンドを実行して、データベースを作成します。
CREATE DATABASE test_db;
Nest.js アプリ用にプロダクトのモジュール、サービス、コントローラーを作成する
データベース接続の構成が完了したので、今度はアプリケーションの詳細構造を構築しましょう。
モジュールを生成する
まず、Product
モジュールを生成します。 この新しいモジュールを使用して、プロダクトに関連するすべてのアイテムをグループ化します。 以下のコマンドを実行します。
nest generate module product
上記のコマンドを実行すると、src
ディレクトリ内に新しい product
フォルダーが作成され、product.module.ts
ファイル内に ProductModule
が定義されます。さらに、app.module.ts
ファイル内のルートモジュールが、新しく作成した ProductModule
をインポートするように更新されます。 ./src/product/product.module.ts
ファイルは、この時点では以下のように空の状態です。
import { Module } from '@nestjs/common';
@Module({})
export class ProductModule {}
エンティティを作成する
TypeORM では、Nest.js アプリケーションに適したデータベーススキーマを作成できるように、エンティティの作成がサポートされています。 エンティティとは、特定のデータベーステーブルにマッピングを行うクラスです。 今回の場合、プロダクトテーブルがエンティティです。
Nest.js アプリ特有の構造に従い、src/product
フォルダーに新しいファイルを作成し、product.entity.ts
という名前を付けます。 そのファイルに以下のコードを貼り付けます。
import { PrimaryGeneratedColumn, BaseEntity, Column, Entity } from 'typeorm';
@Entity()
export class Product extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@Column()
price: string;
}
typeorm
モジュールからインポートしたデコレーターを使用して、プロダクトテーブルに 4 つの列を作成しました。 id 列が、プロダクトを一意に特定する主キー列です。
データ転送オブジェクトを作成する
データ転送オブジェクト (DTO) を利用すると、アプリケーションで受信するデータ用に適切なデータ構造を作成、検証しやすくなります。 たとえば、フロントエンドから Node.js バックエンドに HTTP POST リクエストを送信する場合、フォームから送信されたコンテンツを抽出し解析して、バックエンドコードで利用しやすい形式にする必要があります。 DTO を使うことで、リクエスト本文から抽出されるオブジェクトの形状を指定でき、検証の組み込みが容易になります。
今回のアプリケーションに DTO をセットアップするために、src/product
ディレクトリ内に dto
という名前の新しいフォルダーを作成します。 次に、新しく作成したフォルダー内に create-product.dto.ts
という名前のファイルを作成します。 このファイルに以下を入力します。
import { IsString } from 'class-validator';
export class CreateProductDTO {
@IsString()
name: string;
@IsString()
description: string;
@IsString()
price: string;
}
上記では、CreateProductDTO
を表すクラスを定義したうえで、フィールドのデータ型が string であるか確認する検証を追加しています。 次は、アプリケーションデータベースにデータを直接保存するためのリポジトリを作成します。
カスタムリポジトリを作成する
一般的に、TypeORM などの ORM では主にリポジトリが永続化レイヤーの役割を果たします。 このレイヤーには、次のようなメソッドが含まれます。
save()
delete()
find()
これにより、アプリケーションのデータベースとの通信が容易になります。 このチュートリアルでは、プロダクトエンティティ用に TypeORM のベースリポジトリを拡張したカスタムリポジトリを作成するとともに、目的のクエリに合わせたカスタムメソッドも作成します。 まず、src/product
フォルダーに移動し、product.repository.ts
という名前で新しいファイルを作成します。 ファイルを作成したら、以下のコードをファイルに貼り付けます。
import { Repository, EntityRepository } from 'typeorm';
import { Product } from './product.entity';
import { CreateProductDTO } from './dto/create-product.dto';
@EntityRepository(Product)
export class ProductRepository extends Repository<Product> {
public async createProduct(
createProductDto: CreateProductDTO,
): Promise<Product> {
const { name, description, price } = createProductDto;
const product = new Product();
product.name = name;
product.description = description;
product.price = price;
await product.save();
return product;
}
public async editProduct(
createProductDto: CreateProductDTO,
editedProduct: Product,
): Promise<Product> {
const { name, description, price } = createProductDto;
editedProduct.name = name;
editedProduct.description = description;
editedProduct.price = price;
await editedProduct.save();
return editedProduct;
}
}
上記のコードでは、次の 2 つのメソッドを定義しています。
createProduct()
:createProductDto
クラス (HTTP リクエストの本文を抽出する) を引数として受け取ります。 受け取ったcreateProductDto
を分解し、その値を使用して新しいプロダクトを登録します。editProduct
: このメソッドには、編集が必要なプロダクトの詳細を渡します。クライアント側の新しい値に応じて指定した情報を更新し、データベースに保存します。
Nest.js サービスを生成する
Nest.js では、”関心の分離” の原則に従い、もう 1 つの構成要素としてサービス (プロバイダーとも呼ばれる) が用意されています。 サービスの目的は、複雑なビジネスロジックをコントローラーから分離して扱い、抽象化し、適切な応答を返すことです。 Nest.js のサービスはすべて、@Injectable()
デコレータで修飾します。これにより、他のファイル (コントローラーやモジュールなど) にサービスを簡単に挿入できます。
以下のコマンドを実行して、プロダクト用のサービスを作成します。
nest generate service product
上記のコマンドを実行すると、以下の出力がターミナルに表示されます。
CREATE /src/product/product.service.spec.ts (467 bytes)
CREATE /src/product/product.service.ts (91 bytes)
UPDATE /src/product/product.module.ts (167 bytes)
この nest
コマンドでは、以下の 2 つのファイルが src/product
フォルダー内に作成されます 。
product.service.spec.ts
ファイル: プロダクトのサービスファイル内に作成するメソッド用の単体テストを記述するために使用します。product.service.ts
ファイル: アプリケーションのビジネスロジックをすべて保持します。
また、nest
コマンドにより、新しく作成したサービスがインポートされ、product.module.ts
ファイルに追加されます。
次は、product.service.ts
ファイルに一連のメソッドを記述します。プロダクトを登録するメソッド、すべてのプロダクトを取得するメソッドと、特定のプロダクトの詳細を取得するメソッド、更新するメソッド、削除するメソッドです。 ファイルを開き、内容を以下のコードで置き換えます。
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Product } from './product.entity';
import { CreateProductDTO } from './dto/create-product.dto';
import { ProductRepository } from './product.repository';
@Injectable()
export class ProductService {
constructor(
@InjectRepository(ProductRepository)
private productRepository: ProductRepository,
) {}
public async createProduct(
createProductDto: CreateProductDTO,
): Promise<Product> {
return await this.productRepository.createProduct(createProductDto);
}
public async getProducts(): Promise<Product[]> {
return await this.productRepository.find();
}
public async getProduct(productId: number): Promise<Product> {
const foundProduct = await this.productRepository.findOne(productId);
if (!foundProduct) {
throw new NotFoundException('Product not found');
}
return foundProduct;
}
public async editProduct(
productId: number,
createProductDto: CreateProductDTO,
): Promise<Product> {
const editedProduct = await this.productRepository.findOne(productId);
if (!editedProduct) {
throw new NotFoundException('Product not found');
}
return this.productRepository.editProduct(createProductDto, editedProduct);
}
public async deleteProduct(productId: number): Promise<void> {
await this.productRepository.delete(productId);
}
}
これで、アプリケーションに必要なモジュールのインポートが完了し、以下の各メソッドを作成できました。
createProduct()
: 新しいプロダクトを登録するgetProducts()
: 登録済みのプロダクトをすべて取得するgetProduct()
: 1 つのプロダクトの詳細を取得するeditProduct()
: 特定のプロダクトの詳細を編集するdeleteProduct()
: 1 つのプロダクトを削除する
データベースとのやり取りや通信を簡単に行えるように、先ほど作成した ProductRepository
をこのサービスに挿入している点に着目してください。 具体的には、ファイルの以下の部分でこの処理を行っています。
...
constructor(
@InjectRepository(ProductRepository)
private productRepository: ProductRepository,
) {}
...
ただし、これが機能するのは、プロダクトモジュールに ProductRepository
もインポートしている場合だけです。 インポートは後ほど行います。
Nest.js コントローラーを生成する
Nest.js におけるコントローラーの役割は、アプリケーションのクライアント側からの受信 HTTP リクエストを受け取って処理し、ビジネスロジックに基づいて適切な応答を返すことです。 通常、どのコントローラーがどのリクエストを受け取るのかはルーティングメカニズムによって決まり、このメカニズムは各コントローラーの先頭に付けた @Controller()
デコレーターで制御します。 ターミナルで以下のコマンドを実行して、プロジェクト用に新しいコントローラーファイルを作成します。
nest generate controller product --no-spec
以下の出力が表示されます。
CREATE /src/product/product.controller.ts (103 bytes)
UPDATE /src/product/product.module.ts (261 bytes)
今回、このコントローラー用のテストは作成しません。そのため、コントローラーの .spec.ts
ファイルが作成されないように、nest
コマンドで --no-spec
オプションを指定しています。 src/product/product.controller.ts
ファイルを開き、内容を以下で置き換えます。
import {
Controller,
Post,
Body,
Get,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { ProductService } from './product.service';
import { CreateProductDTO } from './dto/create-product.dto';
import { Product } from './product.entity';
@Controller('product')
export class ProductController {
constructor(private productService: ProductService) {}
@Post('create')
public async createProduct(
@Body() createProductDto: CreateProductDTO,
): Promise<Product> {
const product = await this.productService.createProduct(createProductDto);
return product;
}
@Get('all')
public async getProducts(): Promise<Product[]> {
const products = await this.productService.getProducts();
return products;
}
@Get('/:productId')
public async getProduct(@Param('productId') productId: number) {
const product = await this.productService.getProduct(productId);
return product;
}
@Patch('/edit/:productId')
public async editProduct(
@Body() createProductDto: CreateProductDTO,
@Param('productId') productId: number,
): Promise<Product> {
const product = await this.productService.editProduct(
productId,
createProductDto,
);
return product;
}
@Delete('/delete/:productId')
public async deleteProduct(@Param('productId') productId: number) {
const deletedProduct = await this.productService.deleteProduct(productId);
return deletedProduct;
}
}
このファイルでは、HTTP リクエストの処理に必要なモジュールをインポートし、先ほど作成した ProductService
をコントローラーに挿入しています。 この挿入は、コンストラクターを定義し、その中で ProductService
で定義済みの関数を使用することで行っています。 その後、以下の非同期メソッドを作成しています。
createProduct()
メソッド: クライアント側から送信された POST HTTP リクエストを処理し、データベースに新しいプロダクトを登録して保存します。getProducts()
メソッド: データベースからプロダクトの全リストを取得します。getProduct()
メソッド:productId
をパラメーターとして受け取り、その一意のproductId
が設定されたプロダクトの詳細をデータベースから取得します。editProduct()
メソッド: 特定のプロダクトの詳細を編集します。deleteProduct()
メソッド: プロダクトを特定する一意のproductId
を受け取り、該当するプロダクトをデータベースから削除します。
このコードのもう 1 つのポイントは、定義した非同期メソッドそれぞれについて、メタデータデコレーターを HTTP 動詞に設定していることです。 つまり、Nest.js でリクエストの処理と応答を行うべきメソッドを識別し、参照するためのプレフィックスを設定しているのです。
たとえば、ProductController
には product
プレフィックス、createProduct()
メソッドには create
プレフィックを設定しています。 こうすることで、product/create
(http://localhost:3000/product/create
) に対して行われた GET
リクエストはすべて、createProduct()
メソッドによって処理されます。 ProductController
内で定義した他のメソッドについても、同様のプロセスが行われます。
プロダクトモジュールを更新する
ここまでの手順で、nest
コマンドを使用してコントローラーとサービスを作成し、作成したコントローラーとサービスを ProductModule
に自動的に追加しました。次は、ProductModule
を更新しましょう。 ./src/product/product.module.ts
を開き、内容を以下のコードで置き換えます。
import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductRepository } from './product.repository';
@Module({
imports: [TypeOrmModule.forFeature([ProductRepository])], // add this
controllers: [ProductController],
providers: [ProductService],
})
export class ProductModule {}
このコードでは、ProductRepository
クラスを TypeOrmModule.forFeature()
メソッドに渡しています。 これで ProductRepository
クラスが使えるようになります。
アプリケーションの準備が完了したので、アプリケーションを実行して、これまでに作成したエンドポイントすべてをテストしてみましょう。 ターミナルで以下のコマンドを実行します。
npm run start:dev
これで、http://localhost:3000
でアプリケーションが起動します。 これで、Postman などのツールを使って API をテストできるようになりました。 Postman は、本番環境にデプロイする前に API の動作を確認するためのテストツールです。
Nest.js アプリケーションを使ってプロダクトを登録する
プロダクトの name
、description
、price
を指定した POST HTTP リクエストを、http://localhost:3000/product/create
エンドポイントに対して実行します。
すべてのプロダクトを取得する
登録したプロダクトの全リストを取得するには、GET HTTP リクエスト呼び出しを http://localhost:3000/product/all
に対して行います。
プロダクトを取得する
1 つのプロダクトの詳細を取得するには、GET HTTP リクエストを http://localhost:3000/product/2
エンドポイントに送信します。 末尾の 2
は、目的のプロダクトの productId
です。 他の値を指定してもかまいません。
プロダクトを編集する
PATCH HTTP リクエストを http://localhost:3000/product/edit/2
エンドポイントに送信して、productId
が 2
のプロダクトの詳細を更新します。
Nest.js アプリケーションのテストを作成する
前のセクションで、API が期待どおり機能することを確認できました。このセクションでは、前に作成した ProductService
クラスで定義したメソッドに対するテストを作成します。 アプリケーションのビジネスロジックの大半は ProductService クラスで処理しているので、このクラスをテストするだけで十分でしょう。
Nest.js には組み込みのテスト用インフラストラクチャが用意されているので、テストに関して行うべき構成はほとんどありません。 Nest.js はすべてのテストツールに対応していますが、設定要らずで Jest と統合できるようになっています。 Jest には、アサート関数やモッキング用のテストダブルユーティリティが用意されています。
product.service.spec.ts
ファイルには、現段階では以下のコードが含まれています。
import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from './product.service';
describe('ProductService', () => {
let service: ProductService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ProductService],
}).compile();
service = module.get<ProductService>(ProductService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
それでは、ProductService
内で定義済みのメソッドすべてをカバーするようにテストを追加しましょう。
createProduct メソッドと getProduct メソッドのテストを作成する
ご存知のとおり、このプロジェクトではまだテスト駆動開発アプローチを採用していません。 したがって、ProductService
内のすべてのビジネスロジックが適切なパラメーターを受け取り、期待どおりの応答を返すことを確認するテストを作成します。 まず、product.service.spec.ts
ファイルを開き、内容を以下で置き換えます。
import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from './product.service';
import { ProductRepository } from './product.repository';
import { NotFoundException } from '@nestjs/common';
describe('ProductService', () => {
let productService;
let productRepository;
const mockProductRepository = () => ({
createProduct: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
delete: jest.fn(),
});
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ProductService,
{
provide: ProductRepository,
useFactory: mockProductRepository,
},
],
}).compile();
productService = await module.get<ProductService>(ProductService);
productRepository = await module.get<ProductRepository>(ProductRepository);
});
describe('createProduct', () => {
it('should save a product in the database', async () => {
productRepository.createProduct.mockResolvedValue('someProduct');
expect(productRepository.createProduct).not.toHaveBeenCalled();
const createProductDto = {
name: 'sample name',
description: 'sample description',
price: 'sample price',
};
const result = await productService.createProduct(createProductDto);
expect(productRepository.createProduct).toHaveBeenCalledWith(
createProductDto,
);
expect(result).toEqual('someProduct');
});
});
describe('getProducts', () => {
it('should get all products', async () => {
productRepository.find.mockResolvedValue('someProducts');
expect(productRepository.find).not.toHaveBeenCalled();
const result = await productService.getProducts();
expect(productRepository.find).toHaveBeenCalled();
expect(result).toEqual('someProducts');
});
});
});
このテストではまず、@nestjs/testing
モジュールから Test
パッケージと TestingModule
パッケージをインポートしています。 これで、createTestingModule
メソッドが使用できるようになります。このメソッドにより、先ほどのテスト内で定義したモジュールの役割を果たすテストモジュールを作成します。 TestingModule
では、providers
配列に ProductService
と、ファクトリを使用してカスタムの ProductRepository
をモッキングする mockProductRepository
を指定します。
その後、プロダクトの登録とプロダクトリストの取得ができるか確認する 2 つのテストスイートコンポーネントを作成しています。
アプリケーション内で 1 つのプロダクトを取得および削除する機能をテストするために、もう少しスクリプトを追加しましょう。 product.service.spec.ts
ファイルで、既存のテストスクリプトの直下に以下のコードを追加します。
import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from './product.service';
import { ProductRepository } from './product.repository';
import { NotFoundException } from '@nestjs/common';
describe('ProductService', () => {
...
describe('getProduct', () => {
it('should retrieve a product with an ID', async () => {
const mockProduct = {
name: 'Test name',
description: 'Test description',
price: 'Test price',
};
productRepository.findOne.mockResolvedValue(mockProduct);
const result = await productService.getProduct(1);
expect(result).toEqual(mockProduct);
expect(productRepository.findOne).toHaveBeenCalledWith(1);
});
it('throws an error as a product is not found', () => {
productRepository.findOne.mockResolvedValue(null);
expect(productService.getProduct(1)).rejects.toThrow(NotFoundException);
});
});
describe('deleteProduct', () => {
it('should delete product', async () => {
productRepository.delete.mockResolvedValue(1);
expect(productRepository.delete).not.toHaveBeenCalled();
await productService.deleteProduct(1);
expect(productRepository.delete).toHaveBeenCalledWith(1);
});
});
});
ここでは、指定のプロダクトを取得するために、デフォルト情報を設定した mockProduct
を作成したうえで、プロダクトを取得して削除できることを確認しています。
テストスクリプト全体については、GitHub のこちらのページをご覧ください。
ローカルでテストを実行する
テストを実行する前に、src/app.controller.spec.ts
にある AppController
用のテストファイルを削除しておいてください。チュートリアル後に AppController
用のテストが必要になった場合は、ご自身で作成してください。 では、以下のコマンドでテストを実行しましょう。
npm run test
出力は以下のようになります。
> nest-starter-testing@0.0.1 test /Users/dominic/workspace/personal/circleci-gwp/nest-starter-testing
> jest
PASS src/app.controller.spec.ts
PASS src/product/product.service.spec.ts
Test Suites: 2 passed, 2 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 3.148 s
Ran all test suites.
テストを自動化する
これで、Nest.js 製の RESTful API と、そのビジネスロジックを確認するテストが完成しました。 次は、CircleCI で継続的インテグレーションをセットアップするための設定ファイルを追加しましょう。 継続的インテグレーションを導入することで、コードに対する更新で既存の機能が壊れる事態を回避できます。 GitHub リポジトリにプッシュしたテストが、自動的に実行されるようになるからです。
まず、.circleci
という名前のフォルダーを作成し、その中に config.yml
という名前の新しいファイルを作成します。 作成したファイルを開き、以下のコードを貼り付けます。
version: 2.1
orbs:
node: circleci/node@3.0.0
jobs:
build-and-test:
executor:
name: node/default
steps:
- checkout
- node/install-packages
- run:
command: npm run test
workflows:
build-and-test:
jobs:
- build-and-test
このコードでは、使用する CircleCI のバージョンを指定し、CircleCI の Node Orb を使用して Node.js のセットアップとインストールを行っています。 その後、プロジェクトの依存関係をすべてインストールしています。 最後のコマンドが、テストを実行する実際のテストコマンドです。
CircleCI でプロジェクトをセットアップする
こちらのページにアクセスして、CircleCI のアカウントを作成します。 GitHub の組織 (Organization) に属している場合は、CircleCI でのリポジトリのセットアップに使用する組織を選択してください。
プロジェクトページに移動したら、先ほど GitHub で作成したプロジェクトを探し、[Set Up Project (プロジェクトのセットアップ)] をクリックします。
構成ページが表示されるので、使用する CircleCI 設定ファイルを選択します。 デフォルトでは、メインブランチの .circleci/config.yaml
にある設定ファイルが設定されています。
[Set Up Project (プロジェクトのセットアップ)] をクリックします。
設定ファイルはすでに追加済みなので、プロンプトが表示されたら [Add Manually (手動で追加)] をクリックします。 ここまでの手順を終えると、自動的にパイプラインの実行が開始され、成功するのを確認できます。
このビルドにはジョブが 1 つ (build-and-test
) 含まれています。 ジョブ内のステップは、すべて 1 単位として新しいコンテナまたは仮想マシン内で実行されます。
また、ジョブをクリックするとステップが表示されます。 ステップは実行可能なコマンドの集まりであり、ジョブ内で実行されます。
ステップをクリックすると、さらに詳細が表示されます。 たとえば、npm run test
ステップをクリックすると、以下のようになります。
すべてのテストが正常に実行され、ローカルでテストを実行したときと同じような出力が表示されました。 この後の作業は、プロジェクトに機能を追加し、テストをさらに作成し、GitHub にプッシュすることだけです。 継続的インテグレーションパイプラインが自動的に実行され、テストが行われます。
本記事で学んだ内容をもとに Nest.js で独自の RESTful API を作成する
Nest.js は、優れた Web アプリケーション構造を実現するのに役立つうえ、 チームで作業内容を整理しベストプラクティスを実践するのにも有用です。 このチュートリアルでは、Nest.js を使って RESTful API を作成し、Postman で機能をテストする方法を学習しました。 最後に、テストを複数作成して、CircleCI で自動化しました。
このチュートリアルでテストしたのは ProductService
だけですが、 ぜひ今回得た知識を活かしてアプリケーションの他の部分もテストしてみてください。
今回使用したアプリケーションのソースコードは、GitHub のこちらのページでご覧いただけます。