Skip to content

Commit

Permalink
test(Product): 상품 구매 동시성 제어 테스트 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
zzzzseong committed Feb 2, 2024
1 parent 0e47162 commit 55cb123
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 6 deletions.
6 changes: 0 additions & 6 deletions src/test/java/com/sportsecho/product/ProductTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,4 @@ public interface ProductTest {
.price(TEST_PRICE)
.quantity(TEST_QUANTITY)
.build();
Product ANOTHER_TEST_PRODUCT = Product.builder()
.title(TEST_PRODUCT_TITLE)
.content(TEST_PRODUCT_CONTENT)
.price(TEST_PRICE)
.quantity(TEST_QUANTITY)
.build();
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.sportsecho.purchase.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.sportsecho.common.exception.GlobalException;
import com.sportsecho.hotdeal.service.PurchaseHotdealTest;
import com.sportsecho.member.MemberTest;
import com.sportsecho.member.MemberTestUtil;
import com.sportsecho.member.entity.Member;
import com.sportsecho.member.entity.MemberRole;
import com.sportsecho.member.repository.MemberRepository;
import com.sportsecho.memberProduct.MemberProductTest;
import com.sportsecho.memberProduct.MemberProductTestUtil;
import com.sportsecho.memberProduct.entity.MemberProduct;
import com.sportsecho.memberProduct.repository.MemberProductRepository;
Expand All @@ -23,15 +27,23 @@
import com.sportsecho.purchase.exception.PurchaseErrorCode;
import com.sportsecho.purchase.repository.PurchaseRepository;
import com.sportsecho.purchaseProduct.repository.PurchaseProductRepository;
import jakarta.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

@ActiveProfiles("test")
@SpringBootTest
Expand All @@ -55,6 +67,9 @@ class PurchaseServiceImplV1Test implements MemberTest, ProductTest, PurchaseTest
@Autowired
PurchaseServiceImplV1 purchaseService;

@Autowired
EntityManager em;

Member member;
Product product;
MemberProduct memberProduct;
Expand Down Expand Up @@ -102,6 +117,79 @@ void purchaseTest_success() {
productRepository.findAll().get(0).getQuantity());
}

/**
* 사용자가 자신의 장바구니에 있는 상품의 구매 버튼을 눌렀을때 발생하는 동시성 이슈에 대한 테스트
* 상품 구매 중 상품 version이 바뀌었다면 동시성 처리를 위해 ObjectOptimisticLockingFailureException 발생
* 낙관적락을 이용해 동시성 제어가 잘 이루어지는지 확인하고 데이터 정합성 검증
* */
@Test
@DisplayName("구매 실패 - 동일 상품에 다수의 구매 요청이 들어오는 경우")
void purchaseTest_fail_concurrency() throws InterruptedException {
//given
int numberOfThreads = 10;
int beforeQuantity = product.getQuantity();

/* 테스트용 member 생성 */
Member[] members = new Member[numberOfThreads];
for (int i = 0; i < numberOfThreads; i++) {
members[i] = memberRepository.save(
MemberTestUtil.getTestMember(
TEST_MEMBER_NAME,
TEST_PASSWORD,
TEST_EMAIL + i,
MemberRole.CUSTOMER
)
);
}

/* 테스트용 장바구니 생성 및 저장 */
for (int i = 0; i < numberOfThreads; i++) {
memberProductRepository.save(
MemberProductTestUtil.getMemberProduct(members[i], product)
);
}

/* exception count를 위한 boolean flag */
boolean[] exceptionFlag = new boolean[members.length];

ExecutorService service = Executors.newFixedThreadPool(numberOfThreads);
CountDownLatch latch = new CountDownLatch(numberOfThreads);

//when
for (int i = 0; i < numberOfThreads; i++) {
final int idx = i;
service.execute(() -> {
try {
purchaseService.purchase(requestDto, members[idx]);
/* OptimisticLock과 재고부족으로 발생하는 예외처리 */
} catch(ObjectOptimisticLockingFailureException | GlobalException e) {
exceptionFlag[idx] = true;
} finally {
latch.countDown();
}
});
}

latch.await();
service.shutdown();

int exceptionCount = 0;
for (boolean flag : exceptionFlag) {
if(flag) exceptionCount++;
}

//then
/* 테스트클래스의 product와 서비스 로직의 memberProduct.getProduct()는 quantity의 더티체킹이 진행되지 않음 */
Product findProduct = productRepository.findById(product.getId()).orElse(null);
assertNotNull(findProduct);

/* 데이터 정합성 검증 */
int expectQuantity = beforeQuantity -
(numberOfThreads-exceptionCount)*MemberProductTest.TEST_QUANTITY;
int actualQuantity = findProduct.getQuantity();
assertEquals(expectQuantity, actualQuantity);
}

@Test
@DisplayName("구매 실패 - 장바구니가 비어있음")
void purchaseTest_fail_emptyCart() {
Expand Down

0 comments on commit 55cb123

Please sign in to comment.