はじめに
新規登録とログインの実装が完了したので、ServiceとContollerで単体テストを実装してみました。
実装したコードを確認したい方は以下よりご確認ください。
実装
AuthServiceとUsersControllerのテストを実装します。
AuthService
auth.service.ts
のテストを行うため、同じディレクトリ階層にauth.service.spec.ts
を作成します。
テストはAuthServiceのメソッドが正しく動作しているかどうかを確認することが目的です。
そのため、AuthServiceはUsersServiceのfind()
create()
を使用しているのですが、これらのメソッドが返すデータについてはテスト用のモックで十分です。
fakeUsersService
として、find()
とcreate()
がテスト用に返すデータを作成します。
次に、テストに使用するServiceをmodule
としてまとめます。
作成したfakeUsersService
についてはUsersService
のuseValue
として登録します。
そして、この一連の処理をbeforeEach()
にまとめます。
beforeEach()
はテストの前に毎回実行される処理となります。
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()
で返ってきたデータuser
のpassword
が入力内容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~catch
でsignin()
が失敗したときにエラーをキャッチするかどうか、キャッチしたエラー内容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()
にまとめます。
説明を割愛したテストについても記述すると以下のようになります。
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
が更新されるかどうか」を確認しています。
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);
});
});
参考資料