Skip to content

TDD 실습#1

Merged
dnwls16071 merged 31 commits into
mainfrom
feat/seller-signup-tdd
Jul 15, 2026
Merged

TDD 실습#1
dnwls16071 merged 31 commits into
mainfrom
feat/seller-signup-tdd

Conversation

@dnwls16071

Copy link
Copy Markdown
Contributor

No description provided.

@dnwls16071
dnwls16071 force-pushed the feat/seller-signup-tdd branch from bd501d7 to 2237c4e Compare July 15, 2026 12:53
- Add POST /seller/signUp full HTTP integration specs (35 cases)
- Implement controller → service → repository flow (TDD RED→GREEN)
- Separate Request(web) and Command(application) DTOs
- Split exception handlers per type via @ControllerAdvice (SRP)
- Organize packages on a single layer axis(domain / application / infrastructure / api)
@dnwls16071
dnwls16071 force-pushed the feat/seller-signup-tdd branch from 2237c4e to 75d6a68 Compare July 15, 2026 13:01
Comment on lines +100 to +101
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().accessToken()).isNotNull();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Suggestion — 시나리오보다 약한 단언 (관점 2)

이 테스트의 이름은 "액세스 토큰을 반환한다"인데 단언은 isNotNull()뿐이라, 서버가 빈 문자열 ""을 돌려줘도 통과합니다. 시나리오가 보장하려는 "실제 토큰 값이 있다"는 계약보다 단언이 약합니다.

Suggested change
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().accessToken()).isNotNull();
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().accessToken()).isNotBlank();

JWT 형식은 바로 다음 테스트(액세스_토큰은_JWT_형식을_따른다)가 지키므로, 여기서는 최소 isNotBlank()만으로도 "빈 토큰"이라는 실패를 잡아낼 수 있습니다.

Comment on lines +97 to +103
@ValueSource(strings = {
"invalid-email",
"invalid-email@",
"invalid-email@test",
"invalid-email@test.",
"invalid-email@.com"
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Suggestion — 반복되는 반례 목록은 공유 소스로 (관점 8)

이 무효 이메일 형식 목록이 세 곳에 중복되어 있습니다.

  • 여기(seller/signup) 인라인 @ValueSource
  • shopper/signup/POST_specs.java:88바이트 단위로 동일한 인라인 @ValueSource
  • TestDataSource.invalidEmails() (여기엔 null까지 포함)

이미 @InvalidEmailSource 라는 공유 소스 애노테이션을 만들어 두고 contactEmail 테스트에는 쓰고 있는데, 정작 email 형식 테스트에서는 두 스펙이 같은 목록을 다시 인라인으로 나열합니다. 이메일 정책이 바뀌면 세 곳을 모두 고쳐야 합니다.

TestDataSourcenull을 뺀 invalidEmailFormats()를 추가하고(미지정 케이스는 별도 테스트가 담당하므로), 이를 감싼 @InvalidEmailFormatSource로 두 signup 스펙이 한 줄로 공유하도록 끌어올리는 방향을 권합니다. 반례가 한곳에 모여 정책 변경 시 한 곳만 고치면 됩니다.

참고: username 목록은 seller/shopper 접두사가 달라 동일하지 않으니 그대로 두는 게 맞습니다 — 인라인 @ValueSource가 더 읽기 좋습니다.

Comment on lines +141 to +154
LocalDateTime referenceTime = LocalDateTime.now(UTC);
fixture.registerProduct();

// Act
ResponseEntity<ArrayCarrier<SellerProductView>> response =
fixture.client().exchange(
get("/seller/products").build(),
new ParameterizedTypeReference<>() { }
);

// Assert
SellerProductView actual = requireNonNull(response.getBody()).items()[0];
assertThat(actual.registeredTimeUtc())
.isCloseTo(referenceTime, within(1, SECONDS));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Tech Debt — 실제 현재시각 의존 (관점 7)

LocalDateTime.now(UTC)를 기준값으로 잡고 within(1, SECONDS)로 비교합니다. 기준값 캡처 → HTTP 왕복 → 서버가 등록 시각 부여 사이의 지연이 1초를 넘으면(콜드 스타트/느린 CI/컨텍스트 워밍업) 플래키하게 실패할 수 있습니다. products/id/GET_specs.java:164 도 동일 패턴입니다.

당장 막을 이슈는 아니지만, 다음 중 하나를 권합니다.

  • 프로덕션에 Clock을 주입 가능하게 하고 테스트에서 고정 시계 사용(가장 견고, 프로덕션 변경 필요)
  • 최소한 허용 오차를 within(10, SECONDS) 수준으로 넓혀 왕복 지연 스파이크를 흡수

지금은 값 자체(등록 시각이 대략 "지금"인지)를 검증하려는 의도이므로, 정밀한 1초 창은 검증 의도에 비해 과하게 타이트합니다.

Comment on lines +117 to +119
assertThat(actual.getPath())
.startsWith("/seller/products/")
.matches(endsWithUUID());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Suggestion — 커스텀 Predicate 단언의 빈약한 실패 메시지 (관점 3)

matches(endsWithUUID())는 실패 시 "does not match given predicate" 수준의 메시지만 남겨, 실제 어떤 path가 왔는지·왜 틀렸는지를 말해주지 못합니다. 검증 방법(split → 마지막 세그먼트 → UUID.fromString try/catch)이 본문에서 빠져 의도는 드러나지만, 실패 진단력은 약합니다.

정규식 단언으로 바꾸면 AssertJ가 기대 패턴과 실제 값을 함께 출력해 진단이 쉬워집니다.

Suggested change
assertThat(actual.getPath())
.startsWith("/seller/products/")
.matches(endsWithUUID());
assertThat(actual.getPath())
.startsWith("/seller/products/")
.matches("/seller/products/[0-9a-fA-F-]{36}");

또는 UUID 검증을 도메인 언어의 커스텀 단언(satisfies(pathEndsWithUuid()))으로 끌어올려 ProductAssertions에 모으는 방향도 좋습니다.

@github-actions

Copy link
Copy Markdown

🧪 테스트 코드 품질 리뷰

전반적으로 모범적인 outside-in 통합 테스트입니다. @TddApiTest 합성 애노테이션으로 부트스트랩을 한곳에 묶고, TestFixture(가입→토큰발급→인증)와 도메인 커스텀 단언(isDerivedFrom, conformsToJwtFormat), 선언적 파라미터 소스(@InvalidEmailSource/@InvalidPasswordSource)로 스펙 본문이 명세 문장처럼 읽힙니다. 각 케이스는 Arrange-Act-Assert 3단계로 구조가 뚜렷하고, 메서드명이 곧 계약을 서술합니다. 막을(Blocking) 이슈는 없습니다.

아래는 신뢰성·중복 측면의 개선 제안입니다.

분류 건수
🚫 Blocking 0건
⚠️ Recommended 1건
💡 Suggestions 3건
📝 Tech Debt 1건

⚠️ Recommended

구매자 토큰 발행(POST /shopper/issueToken)의 실패 계약이 테스트되지 않음 (관점 1 — 커버리지 공백)

이 PR에 ShopperIssueTokenController(잘못된 자격증명 → 400 Bad Request) 프로덕션 코드가 추가되었지만, 대응하는 shopper/issueToken/POST_specs.java가 없습니다. 판매자 쪽은 seller/issueToken/POST_specs.java에 5개 시나리오(200 / 토큰 반환 / JWT 형식 / 존재하지 않는 이메일→400 / 잘못된 비밀번호→400)가 온전히 있는데, 구매자 쪽은 비대칭적으로 비어 있습니다.

  • 해피 패스는 TestFixture.issueShopperToken을 통해 /shopper/me 테스트가 간접적으로 지켜주지만,
  • 존재하지 않는 이메일 → 400, 잘못된 비밀번호 → 400, JWT 형식 같은 실패/형식 계약은 어디서도 검증되지 않습니다.

→ 판매자 스펙을 대칭 복제해 shopper/issueToken/POST_specs.java를 추가하길 권합니다. (README에 구매자 토큰 시나리오 절이 없다면 함께 채우는 것도 좋습니다.)

💡 Suggestions

  1. 약한 단언seller/issueToken/POST_specs.java:101, 액세스_토큰을_반환한다isNotNull()만 확인해 빈 문자열도 통과. isNotBlank() 권장. (인라인)
  2. 반복되는 반례 목록 공유 — 무효 이메일 형식 목록이 seller/shopper signup 인라인 @ValueSourceTestDataSource에 3중 중복. 이미 있는 @InvalidEmailSource 패턴처럼 invalidEmailFormats() 공유 소스로 추출 권장. (인라인)
  3. 커스텀 Predicate의 빈약한 실패 메시지seller/products/POST_specs.javaendsWithUUID()는 실패 시 진단 정보가 부족. 정규식 단언 또는 도메인 커스텀 단언으로 대체 권장. (인라인)

📝 Tech Debt

  1. 실제 현재시각 의존seller/products/GET_specs.java:141, products/id/GET_specs.java:164에서 LocalDateTime.now(UTC) + within(1, SECONDS). 느린 CI에서 플래키 가능. Clock 주입 또는 허용 오차 확대 권장. (인라인)

이 리뷰는 테스트 코드 품질만 다룹니다. 프로덕션 로직 자체의 결함은 일반 리뷰 범위입니다.

Comment on lines +100 to +101
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().accessToken()).isNotNull();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Suggestion — 시나리오보다 약한 단언 (관점 2)

이 테스트의 이름은 "액세스 토큰을 반환한다"인데 단언은 isNotNull()뿐이라, 서버가 빈 문자열 ""을 돌려줘도 통과합니다. 시나리오가 보장하려는 "실제 토큰 값이 있다"는 계약보다 단언이 약합니다.

Suggested change
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().accessToken()).isNotNull();
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().accessToken()).isNotBlank();

💡 JWT 형식은 바로 다음 테스트(액세스_토큰은_JWT_형식을_따른다)가 지키므로, 여기서는 최소 isNotBlank()만으로도 "빈 토큰"이라는 실패를 잡아낼 수 있습니다.

Comment on lines +97 to +103
@ValueSource(strings = {
"invalid-email",
"invalid-email@",
"invalid-email@test",
"invalid-email@test.",
"invalid-email@.com"
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Suggestion — 반복되는 반례 목록은 공유 소스로 (관점 8)

이 무효 이메일 형식 목록이 세 곳에 중복되어 있습니다.

  • 여기(seller/signup) 인라인 @ValueSource
  • shopper/signup/POST_specs.java:88바이트 단위로 동일한 인라인 @ValueSource
  • TestDataSource.invalidEmails() (여기에 null까지 포함)

이미 @InvalidEmailSource 라는 훌륭한 공유 소스 애노테이션을 만들어 두고 contactEmail 테스트에는 쓰고 있는데, 정작 email 형식 테스트에서는 두 스펙이 같은 목록을 다시 인라인으로 나열합니다. 이메일 정책이 바뀌면 세 곳을 모두 고쳐야 합니다.

TestDataSourcenull을 뺀 invalidEmailFormats()를 추가하고(미지정 케이스는 별도 테스트가 담당하므로), 이를 감싼 @InvalidEmailFormatSource로 두 signup 스펙이 한 줄로 공유하도록 끌어올리는 방향을 권합니다. 그러면 반례가 한곳에 모여 정책 변경 시 한 곳만 고치면 됩니다.

참고: username 목록은 seller/shopper 접두사가 달라 동일하지 않으니 그대로 두는 게 맞습니다 — 인라인 @ValueSource가 더 읽기 좋습니다.

- 존재하지 않는 이메일로 토큰 발행 시도 → 400 Bad Request
- 잘못된 비밀번호로 토큰 발행 시도 → 400 Bad Request
- 올바른 자격증명 → 200 OK with JWT token
- JWT 형식 검증

Seller issueToken 명세를 기준으로 작성했으며 커버리지 공백 해소
@dnwls16071
dnwls16071 merged commit b6c39bd into main Jul 15, 2026
1 of 2 checks passed
@dnwls16071
dnwls16071 deleted the feat/seller-signup-tdd branch July 15, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant