Skip to content

Comments

[test] 테스트 코드 작성(두영)#373

Merged
Due-IT merged 16 commits intodevelop/befrom
test/#360-test-due
May 8, 2025
Merged

[test] 테스트 코드 작성(두영)#373
Due-IT merged 16 commits intodevelop/befrom
test/#360-test-due

Conversation

@Due-IT
Copy link
Collaborator

@Due-IT Due-IT commented May 4, 2025

#️⃣연관된 이슈

1. 목표

이 PR의 주요 목표는 테스트 커버리지 향상과 코드 안정성 강화입니다.

  • ClubMetricService와 RecruitmentScheduler의 핵심 로직 검증
  • 예외 케이스 처리 강화 (유효하지 않은 입력값, null 안전성 등)
  • 모킹(Mock)을 활용한 단위 테스트로 비즈니스 로직 신뢰성 확보

2. 주요 변경 내용

(1) ClubMetricService 개선

getDailyRanking()

  • n ≤ 0인 경우 빈 리스트 반환하도록 검증 로직 추가
  • 동아리 정보가 없는 경우 null을 포함하여 랭킹 반환 (NPE 방지)

getDailyActiveUser()

  • n ≤ 0인 경우 빈 배열 반환
  • IP 중복 제거 후 일별 활성 사용자 수 계산

(2) RecruitmentScheduler 개선

getScheduledTasks() 추가

  • 예약된 작업(ScheduledFuture) 맵을 외부에서 확인할 수 있도록 오픈
  • 모집 상태 업데이트 시 존재하지 않는 클럽 예외 처리 강화

(3) 테스트 코드 대규모 추가

ClubMetricServiceTest (238줄)

  • 일간/주간/월간 활성 사용자 통계
  • 메트릭 업데이트 및 랭킹 생성 로직 검증

RecruitmentSchedulerTest (159줄)

  • 모집 시작/종료 스케줄링 시간 정확성 검증
  • 작업 취소 및 상태 업데이트 시나리오 테스트

(4) Fixture 클래스 도입

ClubFixture, MetricFixture로 테스트 데이터 생성 간소화

3. 구현 상세

(1) ClubMetricService 로직 검증

활성 사용자 계산
일간/주간/월간: IP기준 중복 제거하여 반환
랭킹 생성 (getDailyRanking)
동아리 조회 시 null 허용 → Optional 처리로 NPE 방지

4. 작은 버그 픽스

ClubMetricService
버그: getDailyRanking(0) 호출 시 EmptyList 대신 전체 랭킹 반환
수정: n ≤ 0 검증 추가 → Collections.emptyList() 반환

5. 요약

이 PR은 테스트 커버리지 확보와 예외 처리 강화에 중점을 두었습니다.
핵심 로직의 신뢰성을 검증하는 400줄 이상의 테스트 코드 추가
경계 조건(Edge Case) 처리로 시스템 안정성 향상
모킹과 Fixture를 활용한 유지보수성 개선

🫡 논의하고 싶은 부분

트러블슈팅 사항이 한 가지 있었습니다. 상황은 다음과 같습니다.
RecruitmentSchedulerTest 코드 작성시 테스트 대상이 되는 객체가 주입받는 객체들 중 scheduledTasks는 recruitmentScheduler가 가지는 "상태"이기 때문에 실제 객체를 이용하여 검증하고 싶었습니다.

처음엔 @Spy를 사용하여 주입을 mockito에게 맡기고 실제 객체처럼 사용하고 싶었으나 다음과 같이 인라인 초기화된 필드는 Mockito에 의해 주입할 수 없다고 합니다.

private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();

따라서, 다음과 같이 프로덕션 코드에 getter를 추가하여 객체를 꺼내어 조회하는 방법을 사용하였습니다.

@BeforeEach
    void setUp() {
        scheduledTasks = recruitmentScheduler.getScheduledTasks();
    }

위 방법의 장단점은 다음과 같습니다.

  • 장점
    • 간단하다
    • 프로덕션 코드의 변경사항이 적다(불변성에는 영향을 미치지 않음)
  • 단점
    • 적더라도 프로덕션 코드의 변경사항이 발생한다

이 외에도 다양한 방법이 있지만, 그나마 대체 가능한 방법은 인라인 초기화를 제거하고 생성자를 통해 초기화하게 한 후, @Spy를 사용하는 방법인것 같습니다. 어떤게 더 나을까요?

Summary by CodeRabbit

  • 버그 수정

    • 잘못된 입력 값(n ≤ 0)에 대해 랭킹 및 활성 사용자 조회 시 빈 결과를 반환하도록 개선하여 불필요한 처리와 오류를 방지했습니다.
  • 리팩터링/스타일

    • 코드 정렬 및 들여쓰기 등 스타일을 통일하여 가독성을 높였습니다.
  • 새 기능

    • 리크루트 스케줄러의 예약된 작업 목록을 조회할 수 있는 기능이 추가되었습니다.
  • 테스트

    • Club, Metric 관련 테스트 픽스처가 추가되어 테스트 작성이 간편해졌습니다.
    • ClubMetricService, RecruitmentScheduler, UserCommandService에 대한 단위 및 통합 테스트가 추가되어 서비스의 안정성이 향상되었습니다.

Due-IT added 14 commits May 1, 2025 00:40
- 메트릭이 이미 존재한다면 metric.update()를 호출하는지 검증합니다.
- 일일 활성 사용자수를 정확히 계산하는지 검증합니다.
- MetricFixture를 통해 ClubMetric을 mock객체로 정의하여 수정불가능한 값의 반환값을 직접 정의합니다.
- 일부 Club 정보가 누락되어도 null 포함하여 정상 동작해야 합니다.
- 반드시 1개 이상의 동아리를 조회해야 합니다.
- 하루 미만의 사용자수 요청시 빈배열을 반환합니다.
- ip 중복을 제거하여 카운트 합니다
- 하루 미만의 사용자수 요청시 빈배열을 반환합니다.
- ip 중복을 제거하여 카운트 합니다
- 같은 동아리에 대해 기존 스케줄이 존재하면 취소합니다.
- 모집 상태 업데이트를 성공하여 repository를 통해 저장합니다.
- 존재하지 않는 클럽 id에 대해 업데이트를 요청할시 예외를 발생합니다.
@Due-IT Due-IT added ✅ Test test 관련(storybook, jest...) 💾 BE Backend labels May 4, 2025
@Due-IT Due-IT self-assigned this May 4, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 4, 2025

Walkthrough

이 변경 사항은 주로 서비스와 테스트 코드에 대한 추가 및 개선으로 구성되어 있습니다. ClubMetricService의 입력값 유효성 검사가 강화되었으며, RecruitmentScheduler에는 예약된 작업을 반환하는 새로운 공개 메서드가 추가되었습니다. UserCommandService는 코드 스타일 및 정렬이 일관성 있게 수정되었습니다. 테스트 측면에서는 ClubFixture, MetricFixture와 같은 테스트 픽스처 클래스가 새로 도입되었고, 주요 서비스에 대한 단위 및 통합 테스트 클래스가 추가되었습니다. 전반적으로 기능 로직 변경보다는 테스트 커버리지 확장과 코드 품질 개선에 중점을 두었습니다.

Changes

파일/그룹 경로 변경 요약
backend/src/main/java/moadong/club/service/ClubMetricService.java getDailyRanking(int n), getDailyActiveUser(int n) 메서드에 입력값 유효성 검사 추가
backend/src/main/java/moadong/club/service/RecruitmentScheduler.java 코드 정렬 및 공백 개선, getScheduledTasks() 공개 메서드 추가
backend/src/main/java/moadong/user/service/UserCommandService.java 코드 스타일 및 정렬 개선, 기능적 변화 없음
backend/src/test/java/moadong/club/fixture/ClubFixture.java Club 엔티티 모킹용 테스트 픽스처 클래스 및 정적 메서드 추가
backend/src/test/java/moadong/club/fixture/MetricFixture.java ClubMetric 엔티티 모킹용 테스트 픽스처 클래스 및 정적 팩토리 메서드 3종 추가
backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java ClubMetricService의 다양한 시나리오를 검증하는 단위 테스트 클래스 추가
backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java RecruitmentScheduler의 예약, 취소, 상태 업데이트 기능에 대한 단위 테스트 클래스 및 중첩 테스트 클래스 추가
backend/src/test/java/moadong/user/service/UserCommandServiceTest.java UserCommandService에 대한 통합 테스트 클래스 추가(현재는 기본 구조 및 예시 테스트만 포함)

Sequence Diagram(s)

sequenceDiagram
    participant Test as ClubMetricServiceTest
    participant Service as ClubMetricService
    participant Repo as ClubMetricRepository
    participant ClubRepo as ClubRepository

    Test->>Service: getDailyRanking(n)
    alt n <= 0
        Service-->>Test: 빈 리스트 반환
    else
        Service->>Repo: find metrics by date
        Service->>ClubRepo: find clubs by id
        Service-->>Test: 랭킹 리스트 반환
    end
Loading
sequenceDiagram
    participant Test as RecruitmentSchedulerTest
    participant Scheduler as RecruitmentScheduler
    participant TaskScheduler as TaskScheduler
    participant ClubRepo as ClubRepository

    Test->>Scheduler: scheduleRecruitment(clubId, start, end)
    Scheduler->>TaskScheduler: schedule(start)
    Scheduler->>TaskScheduler: schedule(end)
    Scheduler-->>Test: 예약된 작업 반환

    Test->>Scheduler: cancelScheduledTask(clubId)
    Scheduler-->>Test: 작업 취소 및 삭제

    Test->>Scheduler: updateRecruitmentStatus(clubId, status)
    Scheduler->>ClubRepo: findById(clubId)
    alt club 존재
        Scheduler-->>Test: 상태 업데이트
    else
        Scheduler-->>Test: 예외 발생
    end
Loading

Suggested labels

🔨 Refactor

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc6cf7a and 86eef41.

📒 Files selected for processing (2)
  • backend/src/main/java/moadong/club/service/RecruitmentScheduler.java (3 hunks)
  • backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/src/main/java/moadong/club/service/RecruitmentScheduler.java
  • backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR improves test coverage and stability by adding extensive tests for ClubMetricService and RecruitmentScheduler while enhancing input validation and error handling. Key changes include:

  • New unit tests for ClubMetricService and RecruitmentScheduler along with fixture classes.
  • Enhanced input validation in ClubMetricService to handle edge cases.
  • A getter added in RecruitmentScheduler for accessing scheduled tasks during testing.

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
backend/src/test/java/moadong/user/service/UserCommandServiceTest.java Adds a basic test for user registration.
backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java Implements scheduling, cancellation, and status update tests for recruitment tasks.
backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java Provides comprehensive tests for daily, weekly, and monthly metric calculations.
backend/src/test/java/moadong/club/fixture/MetricFixture.java Introduces fixture methods for creating club metric mocks.
backend/src/test/java/moadong/club/fixture/ClubFixture.java Introduces a fixture for creating club mocks.
backend/src/main/java/moadong/user/service/UserCommandService.java Cleans up duplicate import in the service code.
backend/src/main/java/moadong/club/service/RecruitmentScheduler.java Adds a getter for scheduledTasks to enable state verification via tests.
backend/src/main/java/moadong/club/service/ClubMetricService.java Adds n ≤ 0 validations to return empty collections for ranking and active user queries.
Comments suppressed due to low confidence (1)

backend/src/main/java/moadong/club/service/RecruitmentScheduler.java:66

  • Exposing the mutable scheduledTasks map directly can lead to unintended modifications; consider returning an unmodifiable view (e.g., Collections.unmodifiableMap(scheduledTasks)) to preserve encapsulation.
public Map<String, ScheduledFuture<?>> getScheduledTasks() { return scheduledTasks; }


// then
verify(scheduledFuture).cancel(false);
assert (scheduledTasks.get(clubId) == null); // 태스크가 맵에서 제거되었는지 확인
Copy link

Copilot AI May 4, 2025

Choose a reason for hiding this comment

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

Consider replacing the built-in assert with a framework assertion (e.g., assertNull) so that the test failure messages are more descriptive and consistent.

Suggested change
assert (scheduledTasks.get(clubId) == null); // 태스크가 맵에서 제거되었는지 확인
assertNull(scheduledTasks.get(clubId)); // 태스크가 맵에서 제거되었는지 확인

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented May 4, 2025

Test Results

14 tests   14 ✅  1s ⏱️
 8 suites   0 💤
 8 files     0 ❌

Results for commit 86eef41.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🔭 Outside diff range comments (2)
backend/src/main/java/moadong/club/service/RecruitmentScheduler.java (2)

45-46: ⚠️ Potential issue

동일한 키로 두 개의 작업이 저장되는 문제 수정 필요

현재 코드에서는 시작 작업과 종료 작업이 동일한 clubId 키로 저장되어 있습니다. 이로 인해 두 번째 put 호출이 첫 번째 호출을 덮어쓰게 되어, 종료 작업만 맵에 저장되는 문제가 있습니다.

다음과 같이 수정하는 것을 제안합니다:

-scheduledTasks.put(clubId, startFuture);
-scheduledTasks.put(clubId, endFuture);
+scheduledTasks.put(clubId + "_start", startFuture);
+scheduledTasks.put(clubId + "_end", endFuture);

또는 복합 키를 사용하지 않고 두 작업을 함께 관리하기 위한 래퍼 클래스를 도입하는 방법도 고려해볼 수 있습니다:

private class RecruitmentTasks {
    private final ScheduledFuture<?> startFuture;
    private final ScheduledFuture<?> endFuture;
    
    public RecruitmentTasks(ScheduledFuture<?> startFuture, ScheduledFuture<?> endFuture) {
        this.startFuture = startFuture;
        this.endFuture = endFuture;
    }
    
    public void cancel() {
        if (startFuture != null) {
            startFuture.cancel(false);
        }
        if (endFuture != null) {
            endFuture.cancel(false);
        }
    }
}

49-54: 🛠️ Refactor suggestion

cancelScheduledTask 메소드 로직 개선 필요

현재 cancelScheduledTask 메소드는 scheduledTasks 맵에서 하나의 항목만 제거하고 취소합니다. 위의 문제와 연결하여, 시작 및 종료 작업을 별도의 키로 저장한다면 이 메소드도 두 작업을 모두 취소하도록 수정해야 합니다.

public void cancelScheduledTask(String clubId) {
-    ScheduledFuture<?> future = scheduledTasks.remove(clubId);
-    if (future != null) {
-        future.cancel(false);
-    }
+    ScheduledFuture<?> startFuture = scheduledTasks.remove(clubId + "_start");
+    if (startFuture != null) {
+        startFuture.cancel(false);
+    }
+    
+    ScheduledFuture<?> endFuture = scheduledTasks.remove(clubId + "_end");
+    if (endFuture != null) {
+        endFuture.cancel(false);
+    }
}
🧹 Nitpick comments (2)
backend/src/test/java/moadong/club/fixture/ClubFixture.java (1)

1-17: 잘 구성된 클럽 테스트 픽스처 클래스

ClubFixture 클래스는 테스트에 필요한 Club 모의 객체를 생성하는 정적 메소드를 제공합니다. 모의 객체의 기본 동작을 설정하여 테스트 코드의 가독성과 유지보수성을 높이는 좋은 방법입니다.

향후 확장을 고려하여, 다양한 시나리오에 대응할 수 있도록 추가적인 팩토리 메소드를 제공하는 것도 고려해 볼 수 있습니다. 예를 들어, 모집 정보나 기타 속성이 설정된 클럽 객체를 생성하는 메소드를 추가할 수 있습니다.

backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java (1)

123-139: 모집상태 업데이트 성공 테스트가 잘 구현되었습니다.

클럽 모집 상태 업데이트 로직이 적절히 테스트되었습니다:

  1. 클럽 조회 메서드 호출 검증
  2. 클럽 저장 메서드 호출 검증

단, 실제 클럽 객체의 상태가 변경되었는지에 대한 검증이 누락되어 있습니다.

아래와 같이 클럽 상태 변경 검증을 추가하는 것을 권장합니다:

            // when
            recruitmentScheduler.updateRecruitmentStatus(clubId, status);

            // then
            verify(clubRepository).findClubById(any(ObjectId.class));
+           // 클럽의 모집 상태가 실제로 변경되었는지 검증
+           ArgumentCaptor<Club> clubCaptor = ArgumentCaptor.forClass(Club.class);
            verify(clubRepository).save(club);
+           verify(clubRepository).save(clubCaptor.capture());
+           assertEquals(status, clubCaptor.getValue().getRecruitmentStatus());
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 793c11f and bc6cf7a.

📒 Files selected for processing (8)
  • backend/src/main/java/moadong/club/service/ClubMetricService.java (2 hunks)
  • backend/src/main/java/moadong/club/service/RecruitmentScheduler.java (2 hunks)
  • backend/src/main/java/moadong/user/service/UserCommandService.java (5 hunks)
  • backend/src/test/java/moadong/club/fixture/ClubFixture.java (1 hunks)
  • backend/src/test/java/moadong/club/fixture/MetricFixture.java (1 hunks)
  • backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java (1 hunks)
  • backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java (1 hunks)
  • backend/src/test/java/moadong/user/service/UserCommandServiceTest.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java (2)
backend/src/test/java/moadong/club/fixture/ClubFixture.java (1)
  • ClubFixture (8-17)
backend/src/test/java/moadong/club/fixture/MetricFixture.java (1)
  • MetricFixture (9-31)
🪛 GitHub Actions: PR Test
backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java

[error] 92-92: Test failure in method 모집_스케줄링_성공(): org.opentest4j.AssertionFailedError: expected: <> but was: <>

🔇 Additional comments (14)
backend/src/main/java/moadong/club/service/RecruitmentScheduler.java (1)

66-68: 테스트를 위한 getScheduledTasks() 메소드 추가 적절

테스트 코드에서 내부 상태를 확인할 수 있도록 scheduledTasks 맵을 반환하는 메소드를 추가한 것은 적절합니다. PR 목표에 명시된 대로 테스트 커버리지를 확장하는 데 도움이 됩니다.

backend/src/test/java/moadong/club/fixture/MetricFixture.java (1)

1-31: 잘 구성된 테스트 픽스처 클래스

MetricFixture 클래스는 Mockito를 사용하여 테스트에 필요한 ClubMetric 모의 객체를 생성하는 정적 메소드를 제공합니다. 다양한 시나리오에 대응할 수 있도록 오버로딩된 메소드를 제공한 점이 좋습니다.

이러한 테스트 픽스처 클래스는 테스트 코드의 가독성을 높이고 중복을 줄이는 좋은 방법입니다. PR 목표에 명시된 대로 테스트 코드 품질 향상에 기여합니다.

backend/src/main/java/moadong/user/service/UserCommandService.java (3)

5-5: import 구문 재정렬

java.util.Date import 구문의 위치가 재정렬되었습니다. 일관된 import 순서는 코드 가독성에 도움이 됩니다.


47-47: 코드 들여쓰기 개선

메서드 매개변수 및 체인 호출의 들여쓰기가 조정되어 코드의 가독성이 향상되었습니다. 이러한 스타일 개선은 코드 베이스의 일관성을 유지하는 데 도움이 됩니다.

Also applies to: 58-59, 62-62, 64-64, 82-82, 91-91, 96-96, 99-99, 114-115, 129-129


105-105: refreshToken 갱신 시 명시적인 Date 객체 생성

RefreshToken 객체를 생성할 때 현재 시간을 나타내는 new Date()를 명시적으로 사용하고 있습니다. 이는 토큰 갱신 시점을 정확히 기록하기 위한 좋은 접근 방식입니다.

backend/src/main/java/moadong/club/service/ClubMetricService.java (2)

109-113: 적절한 입력 유효성 검사 추가.

n이 0 이하인 경우 빈 목록을 반환하는 검증 로직이 잘 추가되었습니다. 이는 불필요한 데이터베이스 조회를 방지하고 예외 상황을 안전하게 처리합니다.


136-140: 적절한 입력 유효성 검사 추가.

n이 0 이하인 경우 빈 배열을 반환하는 검증 로직이 잘 추가되었습니다. 이는 불필요한 데이터베이스 조회를 방지하고 예외 상황을 안전하게 처리합니다.

backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java (6)

41-59: 메트릭 업데이트 테스트가 잘 작성되었습니다.

기존 메트릭이 있을 때 update() 메서드 호출 및 저장 과정이 올바르게 검증되었습니다. Mockito를 활용한 검증 방식이 적절합니다.


61-91: 일일 활성 사용자 수 계산 테스트가 효과적으로 구현되었습니다.

다양한 날짜의 메트릭을 사용하여 일일 활성 사용자 수 계산 로직을 잘 검증하고 있습니다. 특히 특정 인덱스 값과 나머지 인덱스에 대한 검증이 포괄적으로 이루어지고 있습니다.


93-123: 주간 활성 사용자 수 계산 테스트가 효과적으로 구현되었습니다.

주간 단위로 메트릭을 집계하는 로직이 잘 검증되었습니다. 요일을 고려한 주간 계산 로직 테스트가 포함되어 있어 적절합니다.


125-156: 월간 활성 사용자 수 계산 테스트가 효과적으로 구현되었습니다.

월 단위로 메트릭을 집계하는 로직이 잘 검증되었습니다. 다양한 날짜(월 초, 중간, 말일)에 대한 테스트 케이스가 포함되어 적절합니다.


158-195: 일일 랭킹 테스트가 효과적으로 구현되었습니다.

두 가지 중요한 시나리오를 검증하고 있습니다:

  1. 일부 클럽 정보가 누락된 경우에도 null을 포함하여 정상 동작하는지 검증
  2. n 값이 0일 경우 빈 리스트를 반환하는지 검증

이는 실제 서비스에서 발생할 수 있는 다양한 상황을 테스트하는 좋은 접근입니다.


197-236: 일일 활성 사용자 계산 테스트가 효과적으로 구현되었습니다.

두 가지 중요한 케이스를 검증하고 있습니다:

  1. 입력값(n)이 음수인 경우 빈 배열 반환 검증
  2. IP 중복 제거하여 활성 사용자 계산 검증

특히 동일 IP가 여러 번 접속한 경우 중복을 제거하는 로직이 잘 테스트되었습니다.

backend/src/test/java/moadong/club/service/RecruitmentSchedulerTest.java (1)

105-117: 스케줄 취소 테스트가 잘 구현되었습니다.

스케줄된 작업 취소 기능에 대한 테스트가 적절합니다:

  1. 스케줄된 작업이 취소되는지 검증
  2. 작업이 내부 맵에서 제거되는지 검증

간결하면서도 필요한 검증을 모두 수행하고 있습니다.

Comment on lines +29 to +40
@Test
void 회원가입시_id는_8자이상_15자이하여야_한다() {
// //given
// UserRegisterRequest request = new UserRegisterRequest("123",
// "wap123456@",
// "한글좋아",
// "010-1234-5678");
//
// //when & then
// assertDoesNotThrow(()->userCommandService.registerUser(request));
assertThat(3).isEqualTo(3);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

테스트 코드 구현이 완료되지 않았습니다.

현재 테스트 메서드는 실제 ID 길이 검증을 테스트하지 않고 있습니다. 주석 처리된 코드를 완성하여 다음을 테스트해야 합니다:

  1. 8자 미만인 ID에 대한 예외 발생 검증
  2. 15자 초과인 ID에 대한 예외 발생 검증
  3. 유효한 범위(8-15자) ID의 정상 처리 검증

아래와 같이 테스트 코드를 구현하는 것을 권장합니다:

@Test
void 회원가입시_id는_8자이상_15자이하여야_한다() {
-//        //given
-//        UserRegisterRequest request = new UserRegisterRequest("123",
-//            "wap123456@",
-//            "한글좋아",
-//            "010-1234-5678");
-//
-//        //when & then
-//        assertDoesNotThrow(()->userCommandService.registerUser(request));
-        assertThat(3).isEqualTo(3);
+    // given - 7자 ID (최소 길이 미만)
+    UserRegisterRequest tooShortRequest = new UserRegisterRequest(
+        "1234567",
+        "wap123456@",
+        "한글좋아",
+        "010-1234-5678");
+        
+    // given - 16자 ID (최대 길이 초과)
+    UserRegisterRequest tooLongRequest = new UserRegisterRequest(
+        "1234567890123456",
+        "wap123456@",
+        "한글좋아",
+        "010-1234-5678");
+        
+    // given - 유효한 ID (8-15자)
+    UserRegisterRequest validRequest = new UserRegisterRequest(
+        "12345678",
+        "wap123456@",
+        "한글좋아",
+        "010-1234-5678");
+    
+    // when & then - 짧은 ID
+    assertThrows(IllegalArgumentException.class, 
+        () -> userCommandService.registerUser(tooShortRequest));
+        
+    // when & then - 긴 ID
+    assertThrows(IllegalArgumentException.class, 
+        () -> userCommandService.registerUser(tooLongRequest));
+        
+    // when & then - 유효한 ID
+    assertDoesNotThrow(() -> userCommandService.registerUser(validRequest));
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void 회원가입시_id는_8자이상_15자이하여야_한다() {
// //given
// UserRegisterRequest request = new UserRegisterRequest("123",
// "wap123456@",
// "한글좋아",
// "010-1234-5678");
//
// //when & then
// assertDoesNotThrow(()->userCommandService.registerUser(request));
assertThat(3).isEqualTo(3);
}
@Test
void 회원가입시_id는_8자이상_15자이하여야_한다() {
// given - 7자 ID (최소 길이 미만)
UserRegisterRequest tooShortRequest = new UserRegisterRequest(
"1234567",
"wap123456@",
"한글좋아",
"010-1234-5678");
// given - 16자 ID (최대 길이 초과)
UserRegisterRequest tooLongRequest = new UserRegisterRequest(
"1234567890123456",
"wap123456@",
"한글좋아",
"010-1234-5678");
// given - 유효한 ID (8-15자)
UserRegisterRequest validRequest = new UserRegisterRequest(
"12345678",
"wap123456@",
"한글좋아",
"010-1234-5678");
// when & then - 짧은 ID
assertThrows(IllegalArgumentException.class,
() -> userCommandService.registerUser(tooShortRequest));
// when & then - 긴 ID
assertThrows(IllegalArgumentException.class,
() -> userCommandService.registerUser(tooLongRequest));
// when & then - 유효한 ID
assertDoesNotThrow(() -> userCommandService.registerUser(validRequest));
}

Comment on lines +141 to +157
@Test
void 모집상태_업데이트_실패_존재하지않는_클럽() {
// given
String clubId = new ObjectId().toHexString();
ClubRecruitmentStatus status = ClubRecruitmentStatus.OPEN;

when(clubRepository.findClubById(any(ObjectId.class))).thenReturn(Optional.empty());

// when, then
try {
recruitmentScheduler.updateRecruitmentStatus(clubId, status);
} catch (Exception e) {
assert (e instanceof RestApiException);
assert (((RestApiException) e).getErrorCode().equals(
ErrorCode.CLUB_NOT_FOUND));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

모집상태 업데이트 실패 테스트가 잘 구현되었습니다.

존재하지 않는 클럽에 대한 모집 상태 업데이트 실패 시나리오가 적절히 테스트되었습니다.
그러나 테스트 방식이 JUnit의 표준 예외 테스트 방식을 따르지 않고 있습니다.

다음과 같이 JUnit의 표준 예외 테스트 방식으로 변경하는 것을 권장합니다:

            // when, then
-           try {
-               recruitmentScheduler.updateRecruitmentStatus(clubId, status);
-           } catch (Exception e) {
-               assert (e instanceof RestApiException);
-               assert (((RestApiException) e).getErrorCode().equals(
-                   ErrorCode.CLUB_NOT_FOUND));
-           }
+           RestApiException exception = assertThrows(RestApiException.class, () -> 
+               recruitmentScheduler.updateRecruitmentStatus(clubId, status));
+           assertEquals(ErrorCode.CLUB_NOT_FOUND, exception.getErrorCode());

이 방식은 더 간결하고 JUnit의 표준 패턴을 따르며, 테스트 실패 시 더 명확한 오류 메시지를 제공합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void 모집상태_업데이트_실패_존재하지않는_클럽() {
// given
String clubId = new ObjectId().toHexString();
ClubRecruitmentStatus status = ClubRecruitmentStatus.OPEN;
when(clubRepository.findClubById(any(ObjectId.class))).thenReturn(Optional.empty());
// when, then
try {
recruitmentScheduler.updateRecruitmentStatus(clubId, status);
} catch (Exception e) {
assert (e instanceof RestApiException);
assert (((RestApiException) e).getErrorCode().equals(
ErrorCode.CLUB_NOT_FOUND));
}
}
@Test
void 모집상태_업데이트_실패_존재하지않는_클럽() {
// given
String clubId = new ObjectId().toHexString();
ClubRecruitmentStatus status = ClubRecruitmentStatus.OPEN;
when(clubRepository.findClubById(any(ObjectId.class))).thenReturn(Optional.empty());
// when, then
RestApiException exception = assertThrows(RestApiException.class, () ->
recruitmentScheduler.updateRecruitmentStatus(clubId, status));
assertEquals(ErrorCode.CLUB_NOT_FOUND, exception.getErrorCode());
}

Comment on lines 66 to 99
@Test
void 모집_스케줄링_성공() {
// given
String clubId = "club-1";

LocalDateTime startDate = LocalDateTime.now().plusDays(1);
Instant expectedStartInstant = startDate.atZone(ZoneId.of("Asia/Seoul")).toInstant()
.truncatedTo(ChronoUnit.MILLIS);

LocalDateTime endDate = LocalDateTime.now().plusDays(2);
Instant expectedEndInstant = endDate.atZone(ZoneId.of("Asia/Seoul")).toInstant()
.truncatedTo(ChronoUnit.MILLIS);

when(taskScheduler.schedule(any(Runnable.class), any(Date.class)))
.thenReturn((ScheduledFuture) scheduledFuture);

// when
recruitmentScheduler.scheduleRecruitment(clubId, startDate, endDate);

//then
verify(taskScheduler, times(2)).schedule(runnableCaptor.capture(),
dateCaptor.capture());

// 모집 시장 스케줄링 검증
List<Date> dates = dateCaptor.getAllValues();
Date startScheduleDate = dates.get(0);
assertEquals(expectedStartInstant,
startScheduleDate.toInstant());

// 모집 종료 스케줄링 검증
Date endScheduleDate = dates.get(1);
assertEquals(expectedEndInstant,
endScheduleDate.toInstant());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

테스트 실패 해결 필요: 시간대 또는 날짜 비교 문제.

파이프라인 실패 로그에 따르면 모집_스케줄링_성공() 테스트가 92번 라인의 날짜 비교에서 실패하고 있습니다.
expected: <> but was: <> 오류는 시간대 처리나 시간 정밀도 문제일 수 있습니다.

다음 중 하나의 방법으로 문제를 해결해보세요:

  1. 시간 비교 방식 변경:
-            assertEquals(expectedStartInstant,
-                startScheduleDate.toInstant());
+            // 밀리초 단위로 비교하거나 직접 시간 차이를 계산하여 작은 차이는 허용
+            assertTrue(Math.abs(expectedStartInstant.toEpochMilli() - 
+                startScheduleDate.toInstant().toEpochMilli()) < 10);
  1. 캡처된 객체에 Mockito spy 추가:
-            when(taskScheduler.schedule(any(Runnable.class), any(Date.class)))
-                .thenReturn((ScheduledFuture) scheduledFuture);
+            // Date 객체를 전달하기 전에 Spy로 감싸서 실제 값 확인
+            when(taskScheduler.schedule(any(Runnable.class), any(Date.class)))
+                .thenAnswer(invocation -> {
+                    Date date = invocation.getArgument(1);
+                    // 예상되는 시간과 동일한지 확인할 수 있도록 로깅
+                    System.out.println("Scheduled date: " + date.toInstant());
+                    return scheduledFuture;
+                });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void 모집_스케줄링_성공() {
// given
String clubId = "club-1";
LocalDateTime startDate = LocalDateTime.now().plusDays(1);
Instant expectedStartInstant = startDate.atZone(ZoneId.of("Asia/Seoul")).toInstant()
.truncatedTo(ChronoUnit.MILLIS);
LocalDateTime endDate = LocalDateTime.now().plusDays(2);
Instant expectedEndInstant = endDate.atZone(ZoneId.of("Asia/Seoul")).toInstant()
.truncatedTo(ChronoUnit.MILLIS);
when(taskScheduler.schedule(any(Runnable.class), any(Date.class)))
.thenReturn((ScheduledFuture) scheduledFuture);
// when
recruitmentScheduler.scheduleRecruitment(clubId, startDate, endDate);
//then
verify(taskScheduler, times(2)).schedule(runnableCaptor.capture(),
dateCaptor.capture());
// 모집 시장 스케줄링 검증
List<Date> dates = dateCaptor.getAllValues();
Date startScheduleDate = dates.get(0);
assertEquals(expectedStartInstant,
startScheduleDate.toInstant());
// 모집 종료 스케줄링 검증
Date endScheduleDate = dates.get(1);
assertEquals(expectedEndInstant,
endScheduleDate.toInstant());
}
@Test
void 모집_스케줄링_성공() {
// given
String clubId = "club-1";
LocalDateTime startDate = LocalDateTime.now().plusDays(1);
Instant expectedStartInstant = startDate.atZone(ZoneId.of("Asia/Seoul")).toInstant()
.truncatedTo(ChronoUnit.MILLIS);
LocalDateTime endDate = LocalDateTime.now().plusDays(2);
Instant expectedEndInstant = endDate.atZone(ZoneId.of("Asia/Seoul")).toInstant()
.truncatedTo(ChronoUnit.MILLIS);
// Date 객체를 전달하기 전에 Spy로 감싸서 실제 값 확인
when(taskScheduler.schedule(any(Runnable.class), any(Date.class)))
.thenAnswer(invocation -> {
Date date = invocation.getArgument(1);
// 예상되는 시간과 동일한지 확인할 수 있도록 로깅
System.out.println("Scheduled date: " + date.toInstant());
return scheduledFuture;
});
// when
recruitmentScheduler.scheduleRecruitment(clubId, startDate, endDate);
//then
verify(taskScheduler, times(2)).schedule(runnableCaptor.capture(),
dateCaptor.capture());
// 모집 시작 스케줄링 검증
List<Date> dates = dateCaptor.getAllValues();
Date startScheduleDate = dates.get(0);
// 밀리초 단위로 비교하거나 직접 시간 차이를 계산하여 작은 차이는 허용
assertTrue(Math.abs(expectedStartInstant.toEpochMilli() -
startScheduleDate.toInstant().toEpochMilli()) < 10);
// 모집 종료 스케줄링 검증
Date endScheduleDate = dates.get(1);
assertEquals(expectedEndInstant,
endScheduleDate.toInstant());
}
🧰 Tools
🪛 GitHub Actions: PR Test

[error] 92-92: Test failure in method 모집_스케줄링_성공(): org.opentest4j.AssertionFailedError: expected: <> but was: <>

Copy link
Member

@Zepelown Zepelown left a comment

Choose a reason for hiding this comment

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

Mockito 사용이 눈에 띄네요 고생하셨습니다!

Copy link
Collaborator

@PororoAndFriends PororoAndFriends left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! 저도 Mockito 사용법 참고해서 짜야겠네요~

Due-IT added 2 commits May 8, 2025 14:56
- RecruitmentScheduler의 scheduleRecruitment()에서 ZoneId.systemDefault() -> ZoneId.of("Asia/Seoul")로 바꾸어 시간대를 명시함으로써 테스트코드시 시간대가 맞지 않는 플레이키리 현상을 제거하였습니다.
- RecruitmentSchedulerTest에서 ZoneId 상수를 통합하여 사용합니다.
@Due-IT Due-IT merged commit 596c098 into develop/be May 8, 2025
3 checks passed
@lepitaaar lepitaaar deleted the test/#360-test-due branch January 18, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💾 BE Backend ✅ Test test 관련(storybook, jest...)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants