チュートリアルJun 22, 202021 分 READ

Nest.js API の継続的インテグレーション入門

Olususi Oluyemi

フルスタック デベロッパー兼テクニカル ライター

Developer A sits at a desk working on a beginning-level project.

Nest.js とは、TypeScript で構築されたスケーラブルで効率的なサーバーサイド Node.js フレームワークであり、 Node.js の開発で構造に関するデザインパターンを使うために作成されました。 Angular.js から着想を得て、Express.js をベースにしており、 Express.js ミドルウェアの大部分と互換性を持っています。

このチュートリアルでは、Nest.js を使った RESTful API の開発方法を説明します。 この開発を通じて、Nest.js の基本原則と構成要素を身につけられます。 また、各 API エンドポイントのテストを作成するための推奨アプローチも示します。 終わりに、CircleCI を使ってテストプロセスを自動化する方法も紹介します。

前提条件

このチュートリアルを進めるには、以下のものが必要です。

本チュートリアルはさまざまな 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_USERDB_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 アプリケーションを使ってプロダクトを登録する

プロダクトを登録する

プロダクトの namedescriptionprice を指定した 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 エンドポイントに送信して、productId2 のプロダクトの詳細を更新します。

プロダクトを編集する

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 (プロジェクトのセットアップ)] をクリックします。

CircleCI 設定ファイルを選択する

設定ファイルはすでに追加済みなので、プロンプトが表示されたら [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 のこちらのページでご覧いただけます。

クリップボードにコピー