Skip to content

Commit

Permalink
test: 분산락 테스트 작성 (#101)
Browse files Browse the repository at this point in the history
* test: 분산락 테스트 작성

* comment: 일반 주석 -> javadoc

* test: 미션 시작 예고 시간 검증 테스트

* test: 테스트 케이스 추가

* refactor: Component 대신 TestConfiguration과 Bean 사용하도록 변경

* feat: 로그 추가

* fix: where절 오류 수정

* test: MissionRepository 단위 테스트 작성
  • Loading branch information
kimyu0218 authored Nov 26, 2024
1 parent 0187ee0 commit 10f1a00
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.nexters.goalpanzi.application.mission.event.JoinMissionEvent;
import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
Expand All @@ -15,6 +16,7 @@
import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_COMPLETED;
import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_JOINED;

@Slf4j
@RequiredArgsConstructor
@Component
public class PushNotificationEventHandler {
Expand All @@ -30,6 +32,7 @@ void handleJoinMissionEvent(final JoinMissionEvent event) {
MISSION_JOINED.getBody(event.nickname()),
event.deviceToken()
);
log.info("Handled JoinMissionEvent for missionId: {}", event.missionId());
}

@Async
Expand All @@ -42,5 +45,6 @@ void handleCompleteMissionEvent(final CompleteMissionEvent event) {
MISSION_COMPLETED.getBody(),
topic
);
log.info("Handled CompleteMissionEvent for missionId: {}", event.missionId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender;
import com.nexters.goalpanzi.infrastructure.firebase.TopicSubscriber;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
Expand All @@ -32,6 +34,7 @@
import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.*;
import static com.nexters.goalpanzi.domain.mission.MissionStatus.*;

@Slf4j // TODO 오류 확인 후 삭제
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
Expand Down Expand Up @@ -142,9 +145,10 @@ public void viewMissionRank(final Long missionId, final Long memberId) {

@Transactional
public void sendReadyPushMessage() {
LocalDateTime now = LocalDateTime.now();
List<Mission> missions = missionRepository.getReadyMissions();
missions.forEach(mission -> {
if (mission.isReadyTime() && missionValidator.hasEnoughMember(mission.getId())) {
if (mission.isReadyTime(now) && missionValidator.hasEnoughMember(mission.getId())) {
String topic = TopicGenerator.getTopic(mission.getId());
pushNotificationSender.sendGroupMessage(
MISSION_READY.getTitle(),
Expand All @@ -157,15 +161,17 @@ public void sendReadyPushMessage() {

@Transactional
public void sendCancellationWarningPushMessage() {
LocalDateTime now = LocalDateTime.now();
List<Mission> missions = missionRepository.getReadyMissions();
missions.forEach(mission -> {
if (mission.isReadyTime() && !missionValidator.hasEnoughMember(mission.getId())) {
if (mission.isReadyTime(now) && !missionValidator.hasEnoughMember(mission.getId())) {
String topic = TopicGenerator.getTopic(mission.getId());
pushNotificationSender.sendGroupMessage(
MISSION_CANCELLATION_WARNING.getTitle(),
MISSION_CANCELLATION_WARNING.getBody(),
topic
);
log.info("Send CancellationWarningPushMessage to topic: {}", topic);
}
});
}
Expand Down
38 changes: 28 additions & 10 deletions src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,20 @@ public boolean isMissionPeriod() {
return !today.isBefore(missionStart) && !today.isAfter(missionEnd);
}

// 오늘이 미션 인증 요일인지 검증
/**
* <b>오늘이 미션 인증 요일인지 검증</b>
*
* @return 미션 인증 요일 여부
*/
public boolean isMissionDay() {
return this.missionDays.contains(DayOfWeek.valueOf(LocalDate.now().getDayOfWeek().name()));
}

// 현재 시간이 미션 인증 시간인지 검증
/**
* <b>현재 시각이 미션 인증 시간인지 검증</b>
*
* @return 미션 인증 시간 여부
*/
public boolean isMissionTime() {
String now = LocalTime.now().toString().substring(0, 5);
return now.compareTo(uploadStartTime) >= 0 && now.compareTo(uploadEndTime) <= 0;
Expand All @@ -152,20 +160,30 @@ public LocalDateTime getMissionUploadEndDateTime() {
);
}

// 현재 시간이 미션 시작 예고 시간인지 검증
// 미션 시작 예고 시간 == 미션 시작 1시간 전
public boolean isReadyTime() {
/**
* <b>현재 시각이 미션 시작 예고 시간인지 검증</b> <br>
* 미션 시작 예고 시간 == 미션 시작 1시간 전
*
* @param now 현재 시각
* @return 미션 시작 예고 시간 여부
*/
public boolean isReadyTime(final LocalDateTime now) {
LocalDateTime startTime = TimeUtil.combineDateAndTime(
missionStartDate, LocalTime.parse(uploadStartTime)
);
Duration duration = Duration.between(startTime, LocalDateTime.now());
Duration duration = Duration.between(now, startTime);

return duration.isNegative() && duration.toHours() <= 1;
return duration.isPositive() && duration.toHours() <= 1;
}

// 현재 시간이 푸시 시간인지 검증
// 1. 인증 시간이 오전인 경우, 09시에 푸시
// 2. 인증 시간이 오후이거나 종일인 경우, 15시에 푸시
/**
* <b>현재 시간이 푸시 시간인지 검증</b> <br>
* 1. 인증 시간이 오전인 경우, 09시에 푸시 <br>
* 2. 인증 시간이 오후이거나 종일인 경우, 15시에 푸시
*
* @param hour 현재 시각의 시간
* @return 미션 인증 푸시 시간 여부
*/
public boolean isPushTime(final int hour) {
if (uploadStartTime.equals(TimeOfDay.MORNING.getStartTime()) && uploadEndTime.equals(TimeOfDay.MORNING.getEndTime())) {
return hour == PushTime.MORNING.getHour();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface MissionRepository extends JpaRepository<Mission, Long> {

List<Mission> findByMissionStartDateGreaterThanEqual(final LocalDateTime todayStart);

List<Mission> findByMissionStartDateGreaterThanEqualAndMissionEndDateLessThanEqual(final LocalDateTime startDate, final LocalDateTime endDate);
List<Mission> findByMissionStartDateLessThanEqualAndMissionEndDateGreaterThanEqual(final LocalDateTime startDate, final LocalDateTime endDate);

default Mission getMission(final Long missionId) {
return findById(missionId)
Expand All @@ -31,6 +31,6 @@ default List<Mission> getReadyMissions() {

default List<Mission> getInProgressMissions() {
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
return findByMissionStartDateGreaterThanEqualAndMissionEndDateLessThanEqual(todayStart, todayStart);
return findByMissionStartDateLessThanEqualAndMissionEndDateGreaterThanEqual(todayStart, todayStart);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.nexters.goalpanzi.config.redisson;

import com.nexters.goalpanzi.config.redis.RedisInitializer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(
classes = RedissonTestConfig.class
)
@ContextConfiguration(
initializers = {RedisInitializer.class}
)
public class RedissonLockTest {

private static final int THREAD_CNT = 2;

@Autowired
private RedissonLockTestBean redissonLockTestBean;

private ExecutorService executorService;
private AtomicInteger acquiredLockCnt;

@BeforeEach
void setUp() {
executorService = Executors.newFixedThreadPool(THREAD_CNT);
acquiredLockCnt = new AtomicInteger(0);
}

@Test
void 여러_스레드가_동시에_공유_자원에_접근할__없다() throws InterruptedException {
for (int i = 0; i < THREAD_CNT; i++) {
executorService.submit(() -> {
try {
redissonLockTestBean.serializeFunction("Shared Resource");
acquiredLockCnt.incrementAndGet();
} catch (InterruptedException ignored) {
}
});
}
executorService.shutdown();
executorService.awaitTermination(5, TimeUnit.SECONDS);

assertThat(acquiredLockCnt.get()).isNotEqualTo(THREAD_CNT);
}

@Test
void 여러_스레드가_동시에_서로_다른_자원에_접근할__있다() throws InterruptedException {
for (int i = 0; i < THREAD_CNT; i++) {
String resource = "Resource" + i;
executorService.submit(() -> {
try {
redissonLockTestBean.serializeFunction(resource);
acquiredLockCnt.incrementAndGet();
} catch (InterruptedException ignored) {
}
});
}
executorService.shutdown();
executorService.awaitTermination(5, TimeUnit.SECONDS);

assertThat(acquiredLockCnt.get()).isEqualTo(THREAD_CNT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.nexters.goalpanzi.config.redisson;

import com.nexters.goalpanzi.common.annotation.RedissonLock;

public class RedissonLockTestBean {

@RedissonLock(waitTime = 1L)
void serializeFunction(final Object args) throws InterruptedException {
Thread.sleep(2000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.nexters.goalpanzi.config.redisson;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@TestConfiguration
public class RedissonTestConfig {

@Bean
public RedissonLockTestBean redissonLockTestBean() {
return new RedissonLockTestBean();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,24 @@ class MissionTest {
);
assertThat(mission.isPushTime(15)).isTrue();
}

@Test
void 미션_시작까지_1시간__남은_경우_미션_준비_시간이다() {
LocalDateTime now = LocalDateTime.now();
Mission mission = Mission.create(
MEMBER_ID,
DESCRIPTION,
now,
now.plusDays(30),
TimeOfDay.EVERYDAY,
List.of(DayOfWeek.FRIDAY),
BOARD_COUNT,
InvitationCode.generate()
);

assertAll(
() -> assertThat(mission.isReadyTime(mission.getMissionUploadStartDateTime().minusHours(1))).isTrue(),
() -> assertThat(mission.isReadyTime(mission.getMissionUploadStartDateTime().minusMinutes(30))).isTrue()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.nexters.goalpanzi.domain.mission.repository;

import com.nexters.goalpanzi.domain.mission.InvitationCode;
import com.nexters.goalpanzi.domain.mission.Mission;
import com.nexters.goalpanzi.domain.mission.TimeOfDay;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.time.LocalDateTime;
import java.util.List;

import static com.nexters.goalpanzi.fixture.MemberFixture.MEMBER_ID;
import static com.nexters.goalpanzi.fixture.MissionFixture.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

@DataJpaTest
class MissionRepositoryTest {

@Autowired
private MissionRepository missionRepository;

private final Mission READY_MISSION = Mission.create(
MEMBER_ID,
DESCRIPTION,
LocalDateTime.now().plusDays(1),
LocalDateTime.now().plusDays(31),
TimeOfDay.EVERYDAY,
WEEK,
BOARD_COUNT,
InvitationCode.generate()
);
private final Mission IN_PROGRESS_MISSION = Mission.create(
MEMBER_ID,
DESCRIPTION,
LocalDateTime.now().minusDays(1),
LocalDateTime.now().plusDays(29),
TimeOfDay.EVERYDAY,
WEEK,
BOARD_COUNT,
InvitationCode.generate()
);
private final Mission COMPLETED_MISSION = Mission.create(
MEMBER_ID,
DESCRIPTION,
LocalDateTime.now().minusDays(30),
LocalDateTime.now().minusDays(1),
TimeOfDay.EVERYDAY,
WEEK,
BOARD_COUNT,
InvitationCode.generate()
);

@Test
void 준비_상태의_미션을_조회한다() {
Mission readyMission = missionRepository.save(READY_MISSION);
Mission inProgressMission = missionRepository.save(IN_PROGRESS_MISSION);
Mission completedMission = missionRepository.save(COMPLETED_MISSION);

List<Mission> missions = missionRepository.getReadyMissions();

assertAll(
() -> assertThat(missions.size()).isEqualTo(1),
() -> assertThat(missions.get(0).getId()).isEqualTo(readyMission.getId())
);
}

@Test
void 진행중인_미션을_조회한다() {
Mission readyMission = missionRepository.save(READY_MISSION);
Mission inProgressMission = missionRepository.save(IN_PROGRESS_MISSION);
Mission completedMission = missionRepository.save(COMPLETED_MISSION);

List<Mission> missions = missionRepository.getInProgressMissions();

assertAll(
() -> assertThat(missions.size()).isEqualTo(1),
() -> assertThat(missions.get(0).getId()).isEqualTo(inProgressMission.getId())
);
}
}

0 comments on commit 10f1a00

Please sign in to comment.