Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
12 changes: 12 additions & 0 deletions test/unit/point-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ describe('PointService', () => {
NotFoundException,
);
});

it('실패: 잔액 충전 실패 시 에러가 발생해야 한다.', async () => {
const pointDto = { userId: 1, amount: 100 };

userRepository.selectById.mockResolvedValueOnce({ id: 1, balance: 1000 });
// XXX: 재할당하는 것보단 beforeEach에서 초기화하는 것이 좋을 듯..
userRepository.chargeBalanceWithLock = jest
.fn()
.mockRejectedValueOnce(new Error('error'));

await expect(pointService.chargePoint(pointDto)).rejects.toThrow(Error);
});
});

describe('usePointWithLock', () => {
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('상품 재고가 부족합니다.'));
});
});
});
Loading