🐡

[Express, NestJS対応] バックエンドのテスト ~ユニットテスト編~

2022/04/12に公開

シリーズ3部作です。
バックエンドのテスト ~基礎知識編~
バックエンドのテスト ~APIテスト編~
バックエンドのテスト ~ユニットテスト編~ これ

モデルのユニットテスト

次はモデルのユニットテストです。APIテストと打って変わって、データ周りと認証まわりのややこしい部分がないのでだいぶスッキリします。

基本構造

ユーザーモデルです。自己紹介用のメソッドが2つあるとします。ここではモデルをEntityと呼ぶことします。

user.entity.ts
export class UserEntity {
  private name: string;
  
  private permission: string;
  
  constructor(args){
    this.name = args.name;
    this.permission = args.permission;
  }
  
  private hashDecorate (text: string) {
    return `#${text}#`;
  }
  
  public introduceSelf () {
    return `I'm ${this.name}`;
  }
  
  public introduceSelfWithDecorate () {
    return this.hashDecorate(`I'm ${this.name}`);
  }
}

基本的に、privateメソッドはテストせず、publicメソッドをテストします。あまりに詳細にテストしすぎると、テストとコードの結合度が上がって、コードを修正するとすぐにテストが壊れるからです。publicメソッドのインターフェースと返り値なら早々変わることはないだろうという前提があるため、privateメソッドはテストせず、publicメソッドをテストします。

モデルのダミーデータ生成関数です。

buildDummyUser.ts
const dummyNumber = (args?: { min?: number; max?: number }): number => {
  const min = args?.min ?? 0;
  const max = args?.max ?? 2147483647; // 4バイト integer 最大

  return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive
};

const dummyName = (): string => {
  const candidates = [
    'Takeshi',
    'Tawashi',
    'Tada',
    'Yamato',
  ];

  return candidates[number({ min: 0, max: candidates.length - 1 })];
};

export const buildDummyUser = (options) => {
  id: id: options?.id ?? dummyNumber(),
  name: options?.name ?? dummyName(),
}

テストの構造は最初にクラス名、そして各メソッドをネストして書いていくと書きやすいです。

user.test.ts
import { UserEntity } from './userEntity';
import { buildDummyUser } from './buildDummyUser';

describe('UserEntity', () => {

  describe('#introduceSelf', () => {
    test('名前が表示される', () => {
      const user = buildDummyUser({
        name: 'Takeshi'
      });
      
      expect(user.introduceSelf()).toEqual("I'm Takeshi");
    });
  });
  
  describe('#introduceSelfWithDecorate', () => {
    test('名前がデコって表示される', () => {
      const user = buildDummyUser({
        name: 'Takeshi'
      });
      
      expect(user.introduceSelf()).toEqual("#I'm Takeshi#");
    });
  });
});

サービスクラスのユニットテスト

サービスクラスやユースケースクラスを作っている場合は、それらのクラスをユニットテストしたくなるかもしれません。NestJSなどもともとDIコンテナが内蔵されているフレームワークはいいのですが、ExpressなどはDIコンテナは入っていません。DIコンテナを導入していない場合は、jestやsinonjsのstub機能でモジュールごと書き換えることでユニットテストが可能です。もしくは無理にユニットテストをせずに、APIテストでまかうなうのも一つの手かもしれません。

サービスクラスのユニットテストまで書くのは大変であれば、

  • APIテスト
  • モデルのユニットテスト
    だけでもテストしてあると偉いです。

Expressでのサービスクラスのユニットテスト、あまりうまく書けないので、今回はNestJSでのサービスクラスのユニットテストを紹介します。DIコンテナ万歳。

NestJSのあるサービス findUser.service.ts
import { Inject, Injectable, Logger } from '@nestjs/common';

import { UserEntity } from './user.entity';

interface UserRepository {
  findUserById: (args: { userId: number; }) => Promise<UserEntity | null>;
}

@Injectable()
export class FindUserService {
  private logger = new Logger(FindPerformanceMeService.name);

  constructor(
    @Inject('UserRepository') private userRepository: UserRepository,
  ) {}

  do = async ({
    userId
  }: {
    userId: number;
  }): Promise<UserEntity | null> => {
    const user = await this.userRepository.findUserById(
      { userId },
    );

    return user;
  };
}

これをテストしたいと思います。

findUser.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';

import { FindUserService, UserRepository } from './findUser.service';

describe('FindUserService', () => {
  let service: FindUserService;
  let userRepository: UserRepository;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [],
      providers: [FindPerformanceMeService, UserRepository],
    }).compile();

    service = module.get<FindUserService>(FindUserService);
    userRepository = module.get<UserRepository>('UserRepository');
  });

  test('should be defined', () => {
    expect(service).toBeDefined();
  });

  test('do', async () => {
    const user = buildDummyUser();

    const userRepositoryFindUserByIdSpy = jest
      .spyOn(userRepository, 'findUserById')
      .mockResolvedValue(user);

    const res = await service.do({ userId: 1 });

    expect(userRepositoryFindUserByIdSpy).toBeCalled();
    expect(res).toEqual(user);
  });
});

ここではstubを利用してUserRepositoryの処理を偽装しましたが、provider部分に偽のUserRepositoryを代入してもいいと思います。stubだとテスト直前に値をいろいろ入れられるから便利です。

Discussion