LoginSignup
8
3

More than 1 year has passed since last update.

【NestJS】ServiceとControllerの単体テストを実装する

Posted at

はじめに

新規登録とログインの実装が完了したので、ServiceとContollerで単体テストを実装してみました。

実装したコードを確認したい方は以下よりご確認ください。

実装

AuthServiceとUsersControllerのテストを実装します。

AuthService

auth.service.tsのテストを行うため、同じディレクトリ階層にauth.service.spec.tsを作成します。

テストはAuthServiceのメソッドが正しく動作しているかどうかを確認することが目的です。
そのため、AuthServiceはUsersServiceのfind() create()を使用しているのですが、これらのメソッドが返すデータについてはテスト用のモックで十分です。
fakeUsersServiceとして、find()create()がテスト用に返すデータを作成します。

次に、テストに使用するServiceをmoduleとしてまとめます。
作成したfakeUsersServiceについてはUsersServiceuseValueとして登録します。

そして、この一連の処理をbeforeEach()にまとめます。
beforeEach()はテストの前に毎回実行される処理となります。

auth.service.spec.ts
  let service: AuthService;
  let fakeUsersService: Partial<UsersService>;

  beforeEach(async () => {
    const users: User[] = [];
    fakeUsersService = {
      find: (email: string) => {
        const filteredUsers = users.filter((user) => user.email === email);
        return Promise.resolve(filteredUsers);
      },
      create: (email: string, password: string) => {
        const user = {
          id: Math.floor(Math.random() * 999999),
          email,
          password,
        } as User;
        users.push(user);
        return Promise.resolve(user);
      },
    };

    const module = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: UsersService,
          useValue: fakeUsersService,
        },
      ],
    }).compile();

    service = module.get(AuthService);
  });

テストケースは何パターンかありますが、1つには「新規登録したときにパスワードがハッシュされているかどうか」を確認するものがあります。
具体的には以下のアサーションを行っています。

  • signup()で返ってきたデータuserpasswordが入力内容asdfのままではない
  • passwordをsalt部分とhash部分に分けたときにそれぞれに値が入っている
  it('creates a new user with a salted and hashed password', async () => {
    const user = await service.signup('asdf@asdf.com', 'asdf');

    expect(user.password).not.toEqual('asdf');
    const [salt, hash] = user.password.split('.');
    expect(salt).toBeDefined();
    expect(hash).toBeDefined();
  });

また、「間違ったパスワードでログインしたときにエラーが正しく返ってくるかどうか」を確認するテストもあります。
try~catchsignin()が失敗したときにエラーをキャッチするかどうか、キャッチしたエラー内容bad passwordが正しいかどうかを確認します。

  it('throws if an invalid password is provided', async () => {
    await service.signup('asdf@asdf.com', 'sddpetf');

    try {
      await service.signin('asdf@asdf.com', 'sddpetf');
    } catch (err) {
      expect(err).toBeInstanceOf(BadRequestException);
      expect(err.message).toBe('bad password');
    }
  });

beforeEach()や各テストit()はすべてdescribe()にまとめます。
説明を割愛したテストについても記述すると以下のようになります。

auth.service.spec.ts
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { User } from './user.entity';
import { UsersService } from './users.service';

describe('AuthService', () => {
  let service: AuthService;
  let fakeUsersService: Partial<UsersService>;

  beforeEach(async () => {
  ...
  });

  it('can create an instance of auth service', async () => {
    expect(service).toBeDefined();
  });

  it('creates a new user with a salted and hashed password', async () => {
    const user = await service.signup('asdf@asdf.com', 'asdf');

    expect(user.password).not.toEqual('asdf');
    const [salt, hash] = user.password.split('.');
    expect(salt).toBeDefined();
    expect(hash).toBeDefined();
  });

  it('throws an error if user signs up with email that is in use', async () => {
    await service.signup('asdf@asdf.com', 'asdf');

    expect.assertions(2);

    try {
      await service.signup('asdf@asdf.com', 'asdf');
    } catch (err) {
      expect(err).toBeInstanceOf(BadRequestException);
      expect(err.message).toBe('email in use');
    }
  });

  it('throws if signin is called with an unused email', async () => {
    try {
      await service.signin('asdf@asdf.com', 'asdf');
    } catch (err) {
      expect(err).toBeInstanceOf(NotFoundException);
      expect(err.message).toBe('user not found');
    }
  });

  it('throws if an invalid password is provided', async () => {
    await service.signup('asdf@asdf.com', 'sddpetf');

    try {
      await service.signin('asdf@asdf.com', 'sddpetf');
    } catch (err) {
      expect(err).toBeInstanceOf(BadRequestException);
      expect(err.message).toBe('bad password');
    }
  });

  it('returns a user if correct password is provided', async () => {
    await service.signup('asdf@asdf.com', 'mypassword');

    const user = await service.signin('asdf@asdf.com', 'mypassword');
    expect(user).toBeDefined();
  });
});

UsersController

次にControllerのテストを実装します。
やっていることはServiceのテストとほとんど変わりませんが、今回はUsersControllerの動作を確認することが目的なので、使用するAuthServiceのメソッドについてもfakeAuthServiceを作成します。

テストケースを1つ紹介すると、例えば最後のテストでは、「ログインしたときにセッションのuserIDが更新されるかどうか」を確認しています。

users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { AuthService } from './auth.service';
import { User } from './user.entity';
import { NotFoundException } from '@nestjs/common';

describe('UsersController', () => {
  let controller: UsersController;
  let fakeUsersService: Partial<UsersService>;
  let fakeAuthService: Partial<AuthService>;

  beforeEach(async () => {
    fakeUsersService = {
      findOne: (id: number) => {
        return Promise.resolve({
          id,
          email: 'asdf@asdf.com',
          password: 'asdf',
        } as User);
      },
      find: (email: string) => {
        return Promise.resolve([{ id: 1, email, password: 'asdf' } as User]);
      },
      remove: () => {},
      update: () => {},
    };
    fakeAuthService = {
      signup: () => {},
      signin: (email: string, password: string) => {
        return Promise.resolve({ id: 1, email, password } as User);
      },
    };

    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [
        {
          provide: UsersService,
          useValue: fakeUsersService,
        },
        {
          provide: AuthService,
          useValue: fakeAuthService,
        },
      ],
    }).compile();

    controller = module.get<UsersController>(UsersController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  it('findAllUsers returns a list of users with the given email', async () => {
    const users = await controller.findAllUsers('asdf@asdf.com');
    expect(users.length).toEqual(1);
    expect(users[0].email).toEqual('asdf@asdf.com');
  });

  it('findUser returns a single user with the given id', async () => {
    const user = await controller.findUser('1');
    expect(user).toBeDefined();
  });

  it('findUser throws an error if user with given id is not found', async () => {
    fakeUsersService.findOne = () => null;

    try {
      await controller.findUser('1');
    } catch (err) {
      expect(err).toBeInstanceOf(NotFoundException);
      expect(err.message).toBe('user not found');
    }
  });

  it('signin updates session object and returns user', async () => {
    const session = { userId: -10 };
    const user = await controller.signin(
      {
        email: 'asdf@asdf.com',
        password: 'asdf',
      },
      session,
    );

    expect(user.id).toEqual(1);
    expect(session.userId).toEqual(1);
  });
});

参考資料

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3