Merged PR 158: [改善]ユニットテストの方針・実施方法を検討

## 概要
[Task1978: 検討し、Wikiにまとめる](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1978)

- テスト毎にSQLiteのインメモリモードでDBを作成→データ構築→テスト→DBを破棄をすることで、Service~DBを含んだロジックに対するテストを行う
- サンプルとしてTask一覧のテストを何件か実装

## レビューポイント
- 同様な形式で `features/xxx` 毎にテストを作成する時に問題となりそうなものはないか?

## レビュー対象外
- Wikiにまとめる方の作業は別途依頼予定

## 動作確認状況
- ローカルでテストが動作することを確認
This commit is contained in:
湯本 開 2023-06-27 04:25:13 +00:00
parent 01a653015b
commit 5be4995d7d
9 changed files with 1400 additions and 11 deletions

File diff suppressed because it is too large Load Diff

View File

@ -84,6 +84,7 @@
"license-checker": "^25.0.1",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"sqlite3": "^5.1.6",
"supertest": "^6.1.3",
"swagger-ui-express": "^4.5.0",
"ts-jest": "28.0.1",

View File

@ -0,0 +1,89 @@
import { DataSource } from 'typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository.module';
import { AuthModule } from '../../features/auth/auth.module';
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { AccountsModule } from '../../features/accounts/accounts.module';
import { UsersModule } from '../../features/users/users.module';
import { FilesModule } from '../../features/files/files.module';
import { TasksModule } from '../../features/tasks/tasks.module';
import { SendGridModule } from '../../features/../gateways/sendgrid/sendgrid.module';
import { LicensesModule } from '../../features/licenses/licenses.module';
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
import { AudioFilesRepositoryModule } from '../../repositories/audio_files/audio_files.repository.module';
import { AudioOptionItemsRepositoryModule } from '../../repositories/audio_option_items/audio_option_items.repository.module';
import { CheckoutPermissionsRepositoryModule } from '../../repositories/checkout_permissions/checkout_permissions.repository.module';
import { NotificationModule } from '../../features//notification/notification.module';
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
import { AuthGuardsModule } from '../../common/guards/auth/authguards.module';
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module';
import { AuthService } from '../../features/auth/auth.service';
import { AccountsService } from '../../features/accounts/accounts.service';
import { UsersService } from '../../features/users/users.service';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import { FilesService } from '../../features/files/files.service';
import { LicensesService } from '../../features/licenses/licenses.service';
import { TasksService } from '../../features/tasks/tasks.service';
export const makeTestingModule = async (
datasource: DataSource,
): Promise<TestingModule> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'],
isGlobal: true,
}),
AuthModule,
AdB2cModule,
AccountsModule,
UsersModule,
FilesModule,
TasksModule,
UsersModule,
SendGridModule,
LicensesModule,
AccountsRepositoryModule,
UsersRepositoryModule,
LicensesRepositoryModule,
AudioFilesRepositoryModule,
AudioOptionItemsRepositoryModule,
TasksRepositoryModule,
CheckoutPermissionsRepositoryModule,
UserGroupsRepositoryModule,
UserGroupsRepositoryModule,
NotificationModule,
NotificationhubModule,
BlobstorageModule,
AuthGuardsModule,
SortCriteriaRepositoryModule,
],
providers: [
AuthService,
AccountsService,
UsersService,
NotificationhubService,
FilesService,
TasksService,
LicensesService,
],
})
.useMocker(async (token) => {
switch (token) {
case DataSource:
return datasource;
}
})
.compile();
return module;
} catch (e) {
console.log(e);
}
};

View File

@ -6,8 +6,11 @@ import {
} from './test/tasks.service.mock';
import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { TasksService } from './tasks.service';
import { DataSource } from 'typeorm';
import { createAccount, createTask, createUser } from './test/utility';
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { makeTestingModule } from '../../common/test/modules';
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
describe('TasksService', () => {
@ -501,6 +504,174 @@ describe('TasksService', () => {
),
);
});
describe('DBテスト', () => {
let source: DataSource = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
database: ':memory:',
logging: false,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
await source.destroy();
source = null;
});
it('[Admin] Taskが0件であっても実行できる', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { externalId } = await createUser(
source,
accountId,
'userId',
'admin',
);
const service = module.get<TasksService>(TasksService);
const accessToken = { userId: externalId, role: 'admin', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
const { tasks, total } = await service.getTasks(
accessToken,
offset,
limit,
status,
paramName,
direction,
);
expect(tasks).toEqual([]);
expect(total).toEqual(0);
});
it('[Author] Authorは自分が作成者のTask一覧を取得できる', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId } = await createUser(
source,
accountId,
'userId',
'author',
'MY_AUTHOR_ID',
);
await createTask(
source,
accountId,
userId,
'MY_AUTHOR_ID',
'',
'01',
'00000001',
'Uploaded',
);
await createTask(
source,
accountId,
userId,
'MY_AUTHOR_ID',
'',
'01',
'00000002',
'Uploaded',
);
const service = module.get<TasksService>(TasksService);
const accessToken = { userId: 'userId', role: 'author', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded', 'Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
const { tasks, total } = await service.getTasks(
accessToken,
offset,
limit,
status,
paramName,
direction,
);
expect(total).toEqual(2);
{
const task = tasks[0];
expect(task.jobNumber).toEqual('00000001');
}
{
const task = tasks[1];
expect(task.jobNumber).toEqual('00000002');
}
});
it('[Author] Authorは同一アカウントであっても自分以外のAuhtorのTaskは取得できない', async () => {
const module = await makeTestingModule(source);
const { accountId } = await createAccount(source);
const { userId: userId_1 } = await createUser(
source,
accountId,
'userId_1',
'author',
'AUTHOR_ID_1',
);
const { userId: userId_2 } = await createUser(
source,
accountId,
'userId_2',
'author',
'AUTHOR_ID_2',
);
await createTask(
source,
accountId,
userId_1,
'AUTHOR_ID_1',
'',
'01',
'00000001',
'Uploaded',
);
await createTask(
source,
accountId,
userId_2,
'AUTHOR_ID_2',
'',
'01',
'00000002',
'Uploaded',
);
const service = module.get<TasksService>(TasksService);
const accessToken = { userId: 'userId_1', role: 'author', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded', 'Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
const { tasks, total } = await service.getTasks(
accessToken,
offset,
limit,
status,
paramName,
direction,
);
expect(total).toEqual(1);
{
const task = tasks[0];
expect(task.jobNumber).toEqual('00000001');
}
});
});
});
describe('TasksService', () => {

View File

@ -0,0 +1,89 @@
import { DataSource } from 'typeorm';
import { User } from '../../../repositories/users/entity/user.entity';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { Task } from '../../../repositories/tasks/entity/task.entity';
import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity';
export const createAccount = async (
datasource: DataSource,
): Promise<{ accountId: number }> => {
const { identifiers } = await datasource.getRepository(Account).insert({
tier: 1,
country: 'JP',
delegation_permission: false,
locked: false,
company_name: 'test inc.',
verified: true,
deleted_at: '',
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const account = identifiers.pop() as Account;
return { accountId: account.id };
};
export const createUser = async (
datasource: DataSource,
accountId: number,
external_id: string,
role: string,
author_id?: string | undefined,
): Promise<{ userId: number; externalId: string }> => {
const { identifiers } = await datasource.getRepository(User).insert({
account_id: accountId,
external_id: external_id,
role: role,
accepted_terms_version: '1.0',
author_id: author_id,
email_verified: true,
auto_renew: true,
license_alert: true,
notification: true,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const user = identifiers.pop() as User;
return { userId: user.id, externalId: external_id };
};
export const createTask = async (
datasource: DataSource,
account_id: number,
owner_user_id: number,
author_id: string,
work_type_id: string,
priority: string,
jobNumber: string,
status: string,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(AudioFile).insert({
account_id: account_id,
owner_user_id: owner_user_id,
url: '',
file_name: 'x.zip',
author_id: author_id,
work_type_id: work_type_id,
started_at: new Date(),
duration: '100000',
finished_at: new Date(),
uploaded_at: new Date(),
file_size: 10000,
priority: priority,
audio_format: 'audio_format',
is_encrypted: true,
});
const audioFile = identifiers.pop() as AudioFile;
await datasource.getRepository(Task).insert({
job_number: jobNumber,
account_id: account_id,
is_job_number_enabled: true,
audio_file_id: audioFile.id,
status: status,
priority: priority,
created_at: new Date(),
});
};

View File

@ -40,7 +40,7 @@ export class Account {
@Column({ nullable: true })
secondary_admin_user_id?: number;
@Column('timestamp')
@Column({ nullable: true })
deleted_at?: Date;
@Column()

View File

@ -22,7 +22,7 @@ export class LicenseOrder {
@CreateDateColumn()
ordered_at: Date;
@Column('timestamp', { nullable: true })
@Column({ nullable: true })
issued_at?: Date;
@Column()
@ -31,7 +31,7 @@ export class LicenseOrder {
@Column()
status: string;
@Column('timestamp', { nullable: true })
@Column({ nullable: true })
canceled_at?: Date;
}

View File

@ -34,7 +34,7 @@ export class Task {
started_at?: Date;
@Column({ nullable: true })
finished_at?: Date;
@Column({ type: 'timestamp' })
@Column({})
created_at: Date;
@OneToOne(() => AudioFile, (audiofile) => audiofile.task)
@JoinColumn({ name: 'audio_file_id' })

View File

@ -41,7 +41,7 @@ export class User {
@Column()
notification: boolean;
@Column('timestamp', { nullable: true })
@Column({ nullable: true })
deleted_at?: Date;
@Column()