Skip to content

Commit

Permalink
[BE#330] 성능테스트 환경 구축
Browse files Browse the repository at this point in the history
- k6를 통해 지연시간 및 처리량 테스트를 위한 환경 구축
- 여러 사용자를 위한 목유저 생성 스크립트 작성
- 목유저를 DB에 작성하기 위한 ADMIN API 작성
  • Loading branch information
yeongbinim authored Dec 5, 2023
1 parent 06b1532 commit dfe7625
Show file tree
Hide file tree
Showing 19 changed files with 384 additions and 30 deletions.
4 changes: 3 additions & 1 deletion BE/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ lerna-debug.log*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json

loadtest-mock.json
20 changes: 20 additions & 0 deletions BE/src/admin/admin.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';

describe('AdminController', () => {
let controller: AdminController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AdminController],
providers: [AdminService],
}).compile();

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

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
47 changes: 47 additions & 0 deletions BE/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ApiExcludeEndpoint } from '@nestjs/swagger';
import { AuthService } from 'src/auth/auth.service';
import { MATES_MAXIMUM } from 'src/common/const/service-var.const';
import { MatesService } from 'src/mates/mates.service';
import { UsersService } from 'src/users/users.service';

@Controller('admin')
export class AdminController {
constructor(
private readonly authService: AuthService,
private readonly matesService: MatesService,
private readonly usersService: UsersService,
) {}

/**
* mockUser를 생성하기 위한 Admin전용 함수입니다.
*/
@ApiExcludeEndpoint()
@Post('create-mock-user')
async createMockUsers(@Body('emails') emails: Array<{ email: string }>) {
const createdUsers = [];
for (const { email } of emails) {
const { access_token } = await this.authService.loginWithGoogle({
email,
auth_type: 'google',
});
const { nickname } = this.authService.verifyToken(access_token);
createdUsers.push({ email, access_token, nickname });
}

for (let idx = 0; idx < createdUsers.length; idx++) {
const email = createdUsers[idx].email;
const me = await this.usersService.findUserByEmail(email);
for (let i = 1; i <= MATES_MAXIMUM; i++) {
const friendIdx = (idx + i) % createdUsers.length;
const friendNickname = createdUsers[friendIdx].nickname;
try {
await this.matesService.addMate(me, friendNickname);
} catch (e) {
console.log(e);
}
}
}
return createdUsers;
}
}
11 changes: 11 additions & 0 deletions BE/src/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AuthModule } from 'src/auth/auth.module';
import { UsersModule } from 'src/users/users.module';
import { MatesModule } from 'src/mates/mates.module';

@Module({
imports: [AuthModule, UsersModule, MatesModule],
controllers: [AdminController],
})
export class AdminModule {}
2 changes: 2 additions & 0 deletions BE/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ServeStaticModule } from '@nestjs/serve-static';
import { LoggingMiddleware } from './common/middleware/logging.middleware';
import { typeormConfig } from './common/config/typeorm.config';
import { staticConfig } from './common/config/static.config';
import { AdminModule } from './admin/admin.module';

@Module({
imports: [
Expand All @@ -30,6 +31,7 @@ import { staticConfig } from './common/config/static.config';
UsersModule,
PassportModule,
AuthModule,
AdminModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
3 changes: 2 additions & 1 deletion BE/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export class AuthService {
const id = user.email.split('@')[0];
const userEntity = {
nickname:
id + Buffer.from(user.email + user.auth_type).toString('base64'),
id.slice(0, 20) +
Buffer.from(user.email + user.auth_type).toString('base64'),
email: user.email,
} as UsersModel;
const newUser = await this.usersService.createUser(userEntity);
Expand Down
5 changes: 3 additions & 2 deletions BE/src/categories/categories.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CategoryCreateDto } from './dto/request/create-categories.dto';
import { CategoryUpdateDto } from './dto/request/update-categories.dto';
import { CategoryDto } from './dto/response/category.dto';
import { UsersService } from 'src/users/users.service';
import { CATEGORIES_MAXIMUM } from 'src/common/const/service-var.const';

@Injectable()
export class CategoriesService {
Expand All @@ -34,9 +35,9 @@ export class CategoriesService {
const categoryCount = await this.categoriesRepository.count({
where: { user_id: { id: user.id } },
});
if (categoryCount >= 10) {
if (categoryCount >= CATEGORIES_MAXIMUM) {
throw new BadRequestException(
'카테고리는 최대 10개까지 생성할 수 있습니다.',
`카테고리는 최대 ${CATEGORIES_MAXIMUM}개까지 생성할 수 있습니다.`,
);
}

Expand Down
2 changes: 2 additions & 0 deletions BE/src/common/const/service-var.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MATES_MAXIMUM = 10;
export const CATEGORIES_MAXIMUM = 10;
8 changes: 6 additions & 2 deletions BE/src/common/interceptor/logging.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ export class LoggingInterceptor implements NestInterceptor {
const response = ctx.getResponse();
const { method, url, body } = request;

this.logger.debug(`[Request] ${method} ${url} \n ${JSON.stringify(body)}`);
this.logger.debug(
`[Request#${request.id.slice(0, 8)}] ${method} ${url} \n ${JSON.stringify(
body,
)}`,
);
return next.handle().pipe(
tap((body) => {
const requestToResponse: `${number}ms` = `${
Date.now() - request.now
}ms`;
this.logger.debug(
`[Response] ${
`[Response#${request.id.slice(0, 8)}] ${
response.statusCode
} ${requestToResponse} \n ${JSON.stringify(body)} \n`,
);
Expand Down
8 changes: 7 additions & 1 deletion BE/src/common/middleware/logging.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction } from 'express';
import { v4 as uuid } from 'uuid';

@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: Request & { now: number }, res: Response, next: NextFunction) {
use(
req: Request & { now: number; id: string },
res: Response,
next: NextFunction,
) {
req.now = Date.now();
req.id = uuid();
next();
}
}
4 changes: 2 additions & 2 deletions BE/src/common/redis.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export class RedisService {
this.client = createClient();
this.client.connect();
}
set(key: string, value: string) {
this.client.set(key, value);
async set(key: string, value: string) {
await this.client.set(key, value);
}

get(key: string): Promise<string | null> {
Expand Down
5 changes: 2 additions & 3 deletions BE/src/mates/mates.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from '@nestjs/swagger';
import { User } from 'src/users/decorator/user.decorator';
import { MatesService } from './mates.service';
import { MatesDto } from './dto/response/mates.dto';
import { StatusMessageDto } from './dto/response/status-message.dto';
import { AccessTokenGuard } from 'src/auth/guard/bearer-token.guard';
import { UsersModel } from 'src/users/entity/users.entity';
Expand Down Expand Up @@ -96,8 +95,8 @@ export class MatesController {
): Promise<ResponseDto> {
await this.matesService.addMate(user, following_nickname);
return { statusCode: 201, message: '친구가 성공적으로 구독되었습니다.' };
}
}

@Delete('')
@UseGuards(AccessTokenGuard)
@ApiBearerAuth()
Expand Down
1 change: 1 addition & 0 deletions BE/src/mates/mates.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ import { StudyLogsModule } from 'src/study-logs/study-logs.module';
],
controllers: [MatesController],
providers: [MatesService, RedisService],
exports: [MatesService],
})
export class MatesModule {}
7 changes: 5 additions & 2 deletions BE/src/mates/mates.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ConfigService } from '@nestjs/config';
import { ENV } from 'src/common/const/env-keys.const';
import { StudyLogsService } from 'src/study-logs/study-logs.service';
import moment from 'moment';
import { MATES_MAXIMUM } from 'src/common/const/service-var.const';

@Injectable()
export class MatesService {
Expand Down Expand Up @@ -114,8 +115,10 @@ export class MatesService {
where: { follower_id: user },
});

if (matesCount >= 10) {
throw new BadRequestException('친구는 최대 10명까지 추가할 수 있습니다.');
if (matesCount >= MATES_MAXIMUM) {
throw new BadRequestException(
`친구는 최대 ${MATES_MAXIMUM}명까지 추가할 수 있습니다.`,
);
}

const isExist = await this.matesRepository.findOne({
Expand Down
98 changes: 97 additions & 1 deletion BE/src/study-logs/study-logs.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,114 @@
import { Test, TestingModule } from '@nestjs/testing';
import { StudyLogsService } from './study-logs.service';
import { StudyLogs } from './study-logs.entity';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UsersModel } from 'src/users/entity/users.entity';
import { RedisService } from 'src/common/redis.service';
import { BadRequestException } from '@nestjs/common';

describe('StudyLogsService', () => {
let service: StudyLogsService;
let repository: Repository<StudyLogs>;
let usersRepository: Repository<UsersModel>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [StudyLogsService],
providers: [
StudyLogsService,
{
provide: getRepositoryToken(StudyLogs),
useClass: Repository,
},
{
provide: getRepositoryToken(UsersModel),
useClass: Repository,
},
{
provide: RedisService,
useValue: {}, // RedisService를 모킹 (필요한 메서드 제공)
},
],
}).compile();

service = module.get<StudyLogsService>(StudyLogsService);
repository = module.get<Repository<StudyLogs>>(
getRepositoryToken(StudyLogs),
);
usersRepository = module.get<Repository<UsersModel>>(
getRepositoryToken(UsersModel),
);
});

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

describe('.calculateTotalTimes()', () => {
const userId = 1;
const startDate = '2023-01-01';
const endDate = '2023-01-07';
it('유효하지 않은 파라미터에 대해 오류를 발생시켜야 한다', async () => {
expect(service.calculateTotalTimes(null, null, null)).rejects.toThrow(
BadRequestException,
);
expect(service.calculateTotalTimes(1, null, null)).rejects.toThrow(
BadRequestException,
);
expect(
service.calculateTotalTimes(null, startDate, endDate),
).rejects.toThrow(BadRequestException);
});
it('정상적인 범위에서 올바른 결과값을 반환해야 한다.', async () => {
const expectedOutput = [3600, 7200, 0, 0, 3600, 7200, 1800];

jest.spyOn(repository, 'query').mockResolvedValueOnce([
{ date: '2023-01-01', daily_sum: '3600' },
{ date: '2023-01-02', daily_sum: '7200' },
{ date: '2023-01-05', daily_sum: '3600' },
{ date: '2023-01-06', daily_sum: '7200' },
{ date: '2023-01-07', daily_sum: '1800' },
]);

const result = await service.calculateTotalTimes(
userId,
startDate,
endDate,
);

expect(result).toEqual(expectedOutput);
});
it('일치하는 데이터가 없을 때, 0으로 채워진 배열을 반환해야 한다.', async () => {
const expectedOutput = [0, 0, 0, 0, 0, 0, 0];
jest.spyOn(repository, 'query').mockResolvedValueOnce([]);

const result = await service.calculateTotalTimes(
userId,
startDate,
endDate,
);

expect(result).toEqual(expectedOutput);
});
it('단일 날짜에 대해 올바른 학습 시간을 반환해야 한다.', async () => {
const expectedOutput = [3600];

jest
.spyOn(repository, 'query')
.mockResolvedValueOnce([{ date: '2023-01-01', daily_sum: '3600' }]);

const result = await service.calculateTotalTimes(
userId,
startDate,
startDate,
);

expect(result).toEqual(expectedOutput);
});
it('잘못된 날짜 범위에 대해 오류를 발생시켜야 한다.', async () => {
expect(
service.calculateTotalTimes(userId, endDate, startDate),
).rejects.toThrow(BadRequestException);
});
});
});
Loading

0 comments on commit dfe7625

Please sign in to comment.