Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@
import com.daedan.festabook.festival.domain.Festival;
import com.daedan.festabook.festival.domain.FestivalFixture;
import com.daedan.festabook.festival.infrastructure.FestivalJpaRepository;
import com.daedan.festabook.global.lock.ConcurrencyTestHelper;
import com.daedan.festabook.global.security.JwtTestHelper;
import com.daedan.festabook.global.security.role.RoleType;
import com.daedan.festabook.support.AcceptanceTestSupport;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.http.Header;
import io.restassured.response.Response;
import java.util.List;
import java.util.concurrent.Callable;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down Expand Up @@ -453,21 +454,18 @@ class ConcurrentTest {

AnnouncementRequest request = AnnouncementRequestFixture.create(true);

int requestCount = 100;
Runnable httpRequest = () -> {
RestAssured
.given()
.header(authorizationHeader)
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/announcements");
};
Callable<Response> httpRequest = () -> RestAssured
.given()
.header(authorizationHeader)
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/announcements");

int expectedPinnedAnnouncementCount = 3;

// when
ConcurrencyTestHelper.test(requestCount, httpRequest);
concurrencyTestHelper.execute(httpRequest);

// then
Long result = announcementJpaRepository.countByFestivalIdAndIsPinnedTrue(festival.getId());
Expand All @@ -488,31 +486,26 @@ class ConcurrentTest {
AnnouncementRequest createAnnouncement = AnnouncementRequestFixture.create(true);
AnnouncementPinUpdateRequest updateAnnouncement = AnnouncementPinUpdateRequestFixture.create(true);

int requestCount = 100;
Runnable createAnnouncementRequest = () -> {
RestAssured
.given()
.header(authorizationHeader)
.contentType(ContentType.JSON)
.body(createAnnouncement)
.when()
.post("/announcements");
};

Runnable updateAnnouncementRequest = () -> {
RestAssured
.given()
.header(authorizationHeader)
.contentType(ContentType.JSON)
.body(updateAnnouncement)
.when()
.post("announcement/{announcementId}/pin", initAnnouncement.getId());
};
Callable<Response> createAnnouncementRequest = () -> RestAssured
.given()
.header(authorizationHeader)
.contentType(ContentType.JSON)
.body(createAnnouncement)
.when()
.post("/announcements");

Callable<Response> updateAnnouncementRequest = () -> RestAssured
.given()
.header(authorizationHeader)
.contentType(ContentType.JSON)
.body(updateAnnouncement)
.when()
.patch("/announcements/{announcementId}/pin", initAnnouncement.getId());

int expectedPinnedAnnouncementCount = 3;

// when
ConcurrencyTestHelper.test(requestCount, createAnnouncementRequest, updateAnnouncementRequest);
concurrencyTestHelper.execute(createAnnouncementRequest, updateAnnouncementRequest);

// then
Long result = announcementJpaRepository.countByFestivalIdAndIsPinnedTrue(festival.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import com.daedan.festabook.festival.dto.FestivalNotificationRequestFixture;
import com.daedan.festabook.festival.infrastructure.FestivalJpaRepository;
import com.daedan.festabook.festival.infrastructure.FestivalNotificationJpaRepository;
import com.daedan.festabook.global.lock.ConcurrencyTestHelper;
import com.daedan.festabook.support.AcceptanceTestSupport;
import com.daedan.festabook.support.ConcurrencyTestResult;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.Callable;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -47,34 +47,24 @@ class subscribeFestivalNotification {

FestivalNotificationRequest request = FestivalNotificationRequestFixture.create(device.getId());

int requestCount = 100;
AtomicInteger duplicateErrorCount = new AtomicInteger(0);

Runnable httpRequest = () -> {
Response response = RestAssured
.given()
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/festivals/{festivalId}/notifications", festival.getId());

if (response.getStatusCode() == HttpStatus.CONFLICT.value()) {
String responseBody = response.getBody().asString();
if (responseBody.contains("FestivalNotification 데이터베이스에 이미 존재합니다.")) {
duplicateErrorCount.incrementAndGet();
}
}
};
Callable<Response> httpRequest = () -> RestAssured
.given()
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/festivals/{festivalId}/notifications", festival.getId());

// when
ConcurrencyTestHelper.test(requestCount, httpRequest);
ConcurrencyTestResult result = concurrencyTestHelper.execute(httpRequest);

// then
Long result = festivalNotificationJpaRepository.countByFestivalIdAndDeviceId(
Long notificationCount = festivalNotificationJpaRepository.countByFestivalIdAndDeviceId(
festival.getId(), device.getId());
assertThat(result).isEqualTo(1);
assertThat(notificationCount).isEqualTo(1);

assertThat(duplicateErrorCount.get()).isEqualTo(99);
assertThat(result.getSuccessCount()).isEqualTo(1);
assertThat(result.getStatusCodeCount(HttpStatus.CONFLICT))
.isEqualTo(result.getRequestCount() - result.getSuccessCount());
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@Import(TestSecurityConfig.class)
@Import({TestSecurityConfig.class, ConcurrencyTestHelper.class})
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public abstract class AcceptanceTestSupport {

@Autowired
protected ConcurrencyTestHelper concurrencyTestHelper;

@MockitoBean
protected Clock clock;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.daedan.festabook.support;

import io.restassured.response.Response;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ConcurrencyTestHelper {

@Value("${server.tomcat.threads.max}")
private int tomcatThreadCount;

@SafeVarargs
public final ConcurrencyTestResult execute(Callable<Response>... requests) {
validateRequests(requests);
ConcurrencyTestResult result = new ConcurrencyTestResult();

try (ExecutorService executorService = Executors.newFixedThreadPool(tomcatThreadCount)) {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(tomcatThreadCount);

for (int i = 0; i < tomcatThreadCount; i++) {
int currentCount = i % requests.length;
executorService.submit(() -> {
try {
startLatch.await();
Response response = requests[currentCount].call();

HttpStatus httpStatus = HttpStatus.valueOf(response.getStatusCode());
result.recordStatusCode(httpStatus);
} catch (Exception e) {
log.error("동시성 테스트 실행 중 예외 발생", e);
result.recordStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 상황은 API 호출 후, 결과로 INTERNAL_SERVER_ERROR인 상황을 제외하고도 다양할 것 같은데 다른 방법으로 기록하는건 어떤가요?

INTERNAL_SERVER_ERROR를 카운팅하는 것보다는 ConcurrencyTestResult에 필드를 추가해서 정상적으로 테스트가 종료된 횟수라던가, 아니면 문제가 발생했던 횟수라던가를 추가해서 확인하는게 더 정확할 것 같아요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

부기! 좋은 인사이트 감사합니다.

네이밍 명칭을 고민하다가 errorCount 라고 지칭했는데, 피드백해줘요!

test: Error Count 및 Cause 추가

} finally {
endLatch.countDown();
}
});
}

startLatch.countDown();
endLatch.await();
} catch (InterruptedException ignore) {
}
return result;
}

@SafeVarargs
private void validateRequests(Callable<Response>... requests) {
if (requests == null || requests.length == 0) {
throw new IllegalArgumentException("실행할 API 인자는 최소 1개 이상이어야 합니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.daedan.festabook.support;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.HttpStatus;

public class ConcurrencyTestResult {

private final Map<HttpStatus, Integer> statusCodeCounts = new ConcurrentHashMap<>();
private final AtomicLong requestCount = new AtomicLong();

public void recordStatusCode(HttpStatus status) {
statusCodeCounts.merge(status, 1, Integer::sum);
requestCount.incrementAndGet();
}

public int getStatusCodeCount(HttpStatus status) {
return statusCodeCounts.getOrDefault(status, 0);
}

public int getSuccessCount() {
return statusCodeCounts.entrySet().stream()
.filter(entry -> entry.getKey().is2xxSuccessful())
.mapToInt(Map.Entry::getValue)
.sum();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 StatusCodeCount를 강제하는게 어때요?

  • 성공하는 경우가, 200번대는 맞겠지만 실제로 200번대 응답이 뒤섞였을 가능성이 여기서는 알 수 없을 것 같아요

예를 들어 성공 예상은 200응답 11개지만, 200 응답 10개, 201응답 1개가 되었을 때 해당 메서드를 사용하면 모를 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

부기 말대로 200번대는 맞지만, 오해의 소지가 있을 수 있다고 생각이 들어요.

그렇다면, 1차적으로는 삭제했지만, 또 다른 생각 방향으로는 is2xxSuccessful 네이밍으로 변경하는 건 어떤가요?

현재는 해당 메서드 삭제했습니다.


public long getRequestCount() {
return requestCount.get();
}
}
5 changes: 5 additions & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ secret:
fcm:
topic:
festival-prefix: example-festival

server:
tomcat:
threads:
max: 15