Skip to content

Commit

Permalink
test: 상품 서비스, 주문 파사드 tc 추가 및 예외 구체화
Browse files Browse the repository at this point in the history
  • Loading branch information
anniemon committed Jan 11, 2025
1 parent 3915987 commit a1550e4
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 35 deletions.
67 changes: 63 additions & 4 deletions test/unit/order-facade.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DataSource } from 'typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import { OrderFacade } from '@domain/usecase';
import { OrderService, PointService, ProductService } from '@domain/services';
import { BadRequestException, NotFoundException } from '@nestjs/common';

describe('OrderFacade', () => {
let orderFacade: OrderFacade;
Expand Down Expand Up @@ -164,15 +165,15 @@ describe('OrderFacade', () => {
});

describe('createOrderWithTransaction: 주문 생성 실패', () => {
it('모든 상품의 재고가 없으면 주문 생성이 실패하고 트랜잭션이 롤백되어야 한다.', async () => {
it('모든 상품의 재고가 없으면 NotFoundException을 발생시키고, 트랜잭션이 롤백되어야 한다.', async () => {
const userId = 1;
const items = [
{ productId: 1, quantity: 1 },
{ productId: 2, quantity: 2 },
];

productService.decrementStockWithLock.mockRejectedValueOnce(
new Error('Out of stock'),
productService.decrementStockWithLock.mockRejectedValue(
new NotFoundException('상품 재고가 부족합니다.'),
);
productService.findProductsByIdsWithStock.mockResolvedValueOnce([]);
pointService.usePointWithLock.mockResolvedValueOnce({
Expand All @@ -186,7 +187,7 @@ describe('OrderFacade', () => {
userId,
items,
}),
).rejects.toThrow(Error);
).rejects.toThrow(new NotFoundException('상품 재고가 부족합니다.'));

expect(queryRunner.rollbackTransaction).toHaveBeenCalled();
expect(queryRunner.release).toHaveBeenCalled();
Expand Down Expand Up @@ -219,6 +220,7 @@ describe('OrderFacade', () => {
items,
}),
).rejects.toThrow(Error);

expect(queryRunner.rollbackTransaction).toHaveBeenCalled();
expect(queryRunner.release).toHaveBeenCalled();
});
Expand All @@ -244,6 +246,63 @@ describe('OrderFacade', () => {
await expect(
orderFacade.createOrderWithTransaction({ userId, items }),
).rejects.toThrow(Error);

expect(queryRunner.rollbackTransaction).toHaveBeenCalled();
expect(queryRunner.release).toHaveBeenCalled();
});

it('포인트 부족 시 BadRequestException을 발생시키고, 트랜잭션이 롤백되어야 한다.', async () => {
const userId = 1;
const items = [
{ productId: 1, quantity: 1 },
{ productId: 2, quantity: 2 },
];

productService.decrementStockWithLock.mockResolvedValueOnce({
inStockProductIds: [1, 2],
outOfStockProductIds: [],
});
productService.findProductsByIdsWithStock.mockResolvedValueOnce([
{ id: 1, price: 100, quantity: 1 },
{ id: 2, price: 200, quantity: 2 },
]);
pointService.usePointWithLock.mockRejectedValueOnce(
new BadRequestException('잔액이 부족합니다.'),
);
orderService.createOrder.mockResolvedValueOnce({ id: 1, userId, items });

await expect(
orderFacade.createOrderWithTransaction({ userId, items }),
).rejects.toThrow(new BadRequestException('잔액이 부족합니다.'));

expect(queryRunner.rollbackTransaction).toHaveBeenCalled();
expect(queryRunner.release).toHaveBeenCalled();
});

it('유효하지 않은 사용자일 경우, NotFoundException을 발생시키고, 트랜잭션이 롤백되어야 한다.', async () => {
const userId = 1;
const items = [
{ productId: 1, quantity: 1 },
{ productId: 2, quantity: 2 },
];

pointService.usePointWithLock.mockRejectedValueOnce(
new NotFoundException('사용자를 찾을 수 없습니다.'),
);
productService.decrementStockWithLock.mockResolvedValueOnce({
inStockProductIds: [1, 2],
outOfStockProductIds: [],
});
productService.findProductsByIdsWithStock.mockResolvedValueOnce([
{ id: 1, price: 100, quantity: 1 },
{ id: 2, price: 200, quantity: 2 },
]);
orderService.createOrder.mockResolvedValueOnce({ id: 1, userId, items });

await expect(
orderFacade.createOrderWithTransaction({ userId, items }),
).rejects.toThrow(new NotFoundException('사용자를 찾을 수 없습니다.'));

expect(queryRunner.rollbackTransaction).toHaveBeenCalled();
expect(queryRunner.release).toHaveBeenCalled();
});
Expand Down
165 changes: 134 additions & 31 deletions test/unit/product-service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from '@domain/services';
import { ProductRepository, StockRepository } from '@domain/repositories';
import { OutOfStockException } from '@domain/exceptions';
import { NotFoundException } from '@nestjs/common';

describe('ProductService', () => {
let productService: ProductService;
Expand Down Expand Up @@ -32,43 +34,144 @@ describe('ProductService', () => {
productService = moduleFixture.get(ProductService);
});

it('getProductByIdWithStock: 상품 아이디로 상품 조회 메서드를 호출해야 한다.', async () => {
const productId = 1;
const product = await productService.getProductByIdWithStock(productId);
afterEach(() => {
jest.clearAllMocks();
});

describe('상품 조회 성공', () => {
it('getProductByIdWithStock: 상품 아이디로 상품 조회 메서드를 호출해야 한다.', async () => {
const productId = 1;
const product = await productService.getProductByIdWithStock(productId);

expect(product.id).toBe(productId);
expect(productRepository.getProductByIdWithStock).toHaveBeenCalledWith(
productId,
);
expect(productRepository.getProductByIdWithStock).toHaveBeenCalledTimes(
1,
);
});

it('findProductsByIdsWithStock: 상품 아이디 배열로 상품 조회 메서드를 호출해야 한다.', async () => {
const productIds = [1, 2];
const products =
await productService.findProductsByIdsWithStock(productIds);

expect(products.length).toBe(productIds.length);
expect(productRepository.findProductsByIdsWithStock).toHaveBeenCalledWith(
productIds,
);
expect(
productRepository.findProductsByIdsWithStock,
).toHaveBeenCalledTimes(1);
});

it('findProductsByIdsWithStock: 상품이 없을 떄 빈 배열을 반환해야 한다.', async () => {
const productIds = [1, 2];
productRepository.findProductsByIdsWithStock.mockResolvedValueOnce([]);
const products =
await productService.findProductsByIdsWithStock(productIds);

expect(products.length).toBe(0);
});
});

describe('상품 조회 실패', () => {
it('getProductByIdWithStock: 상품 조회 에러 발생 시 에러가 발생해야 한다.', async () => {
productRepository.getProductByIdWithStock.mockRejectedValueOnce(
new Error(),
);
const productId = 1;

await expect(
productService.getProductByIdWithStock(productId),
).rejects.toThrow();
});

expect(product.id).toBe(productId);
expect(productRepository.getProductByIdWithStock).toHaveBeenCalledWith(
productId,
);
expect(productRepository.getProductByIdWithStock).toHaveBeenCalledTimes(1);
it('getProductByIdWithStock: 상품이 없을 때 NotFoundException이 발생해야 한다.', async () => {
productRepository.getProductByIdWithStock.mockResolvedValueOnce(null);
const productId = 1;

await expect(
productService.getProductByIdWithStock(productId),
).rejects.toThrow(NotFoundException);
});

it('findProductsByIdsWithStock: 상품 조회 에러 발생 시 에러가 발생해야 한다.', async () => {
productRepository.findProductsByIdsWithStock.mockRejectedValueOnce(
new Error(),
);
const productIds = [1, 2];

await expect(
productService.findProductsByIdsWithStock(productIds),
).rejects.toThrow(new Error());
});
});

it('findProductsByIdsWithStock: 상품 아이디 배열로 상품 조회 메서드를 호출해야 한다.', async () => {
const productIds = [1, 2];
const products =
await productService.findProductsByIdsWithStock(productIds);

expect(products.length).toBe(productIds.length);
expect(productRepository.findProductsByIdsWithStock).toHaveBeenCalledWith(
productIds,
);
expect(productRepository.findProductsByIdsWithStock).toHaveBeenCalledTimes(
1,
);
describe('상품 재고 차감 성공', () => {
it('decrementStockWithLock: 주문 상품 갯수만큼 상품 재고 차감 메서드를 호출해야 한다.', async () => {
const items = [
{ productId: 1, quantity: 1 },
{ productId: 2, quantity: 2 },
];
await productService.decrementStockWithLock({
items,
queryRunner: null,
});

expect(stockRepository.decrementStockWithLock).toHaveBeenCalledTimes(
items.length,
);
});

it('decrementStockWithLock: 재고가 있는 상품과 없는 상품을 구분하여 반환한다.', async () => {
const items = [
{ productId: 1, quantity: 0 },
{ productId: 2, quantity: 2 },
];
stockRepository.decrementStockWithLock.mockRejectedValueOnce(
new OutOfStockException(),
);

const res = await productService.decrementStockWithLock({
items,
queryRunner: null,
});

expect(res.inStockProductIds).toEqual([2]);
expect(res.outOfStockProductIds).toEqual([1]);
});
});

it('decrementStockWithLock: 주문 상품 갯수만큼 상품 재고 차감 메서드를 호출해야 한다.', async () => {
const items = [
{ productId: 1, quantity: 1 },
{ productId: 2, quantity: 2 },
];
await productService.decrementStockWithLock({
items,
queryRunner: null,
describe('상품 재고 차감 실패', () => {
it('decrementStockWithLock: 상품 재고 차감 에러 발생 시 에러가 발생해야 한다.', async () => {
stockRepository.decrementStockWithLock.mockRejectedValueOnce(new Error());
const items = [{ productId: 1, quantity: 1 }];

await expect(
productService.decrementStockWithLock({
items,
queryRunner: null,
}),
).rejects.toThrow();
});

expect(stockRepository.decrementStockWithLock).toHaveBeenCalledTimes(
items.length,
);
it('decrementStockWithLock: 모든 상품이 재고가 없을 때 NotFoundException이 발생해야 한다.', async () => {
const items = [
{ productId: 1, quantity: 1 },
{ productId: 2, quantity: 1 },
];
stockRepository.decrementStockWithLock.mockRejectedValue(
new OutOfStockException(),
);

await expect(
productService.decrementStockWithLock({
items,
queryRunner: null,
}),
).rejects.toThrow(new NotFoundException('상품 재고가 부족합니다.'));
});
});
});

0 comments on commit a1550e4

Please sign in to comment.