Skip to content
Open
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
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
@@ -1,6 +1,7 @@
package com.daedan.festabook.festival.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

import com.daedan.festabook.device.domain.Device;
import com.daedan.festabook.device.domain.DeviceFixture;
Expand All @@ -11,12 +12,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 +48,28 @@ 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(duplicateErrorCount.get()).isEqualTo(99);
int successCount = result.getStatusCodeCount(HttpStatus.CREATED);

assertAll(
() -> assertThat(notificationCount).isEqualTo(1),
() -> assertThat(successCount).isEqualTo(1),
() -> assertThat(result.getStatusCodeCount(HttpStatus.CONFLICT))
.isEqualTo(result.getRequestCount() - successCount)
);
}
}
}

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.recordError(e);
} 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,34 @@
package com.daedan.festabook.support;

import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
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();
private final AtomicLong errorCount = new AtomicLong();
private final Queue<Throwable> errorCauses = new ConcurrentLinkedQueue<>();

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

public void recordError(Throwable e) {
errorCount.incrementAndGet();
errorCauses.add(e);
}

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

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