쿠폰 엔터티 발급 기능
이제부터 본격적으로 coupon 엔터티에 기능을 추가해보자. 서비스에 기능을 추가할 수 있지만, DDD 관점에서 설정을 한다면 도메인과 관련된 기능은 도메인에 넣어주는 것이 좋다.
- Coupon
// mycouponcore/model/Coupon.java
package com.example.mycouponcore.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "coupons")
public class Coupon extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private CouponType couponType;
private Integer totalQuantity;
@Column(nullable = false)
private int issuedQuantity;
@Column(nullable = false)
private int discountAmount;
@Column(nullable = false)
private int minAvailableAmount;
@Column(nullable = false)
private LocalDateTime dateIssueStart;
@Column(nullable = false)
private LocalDateTime dateIssueEnd;
// 발행 가능한지 기간 검증
public boolean availableIssueDate() {
LocalDateTime now = LocalDateTime.now();
return dateIssueStart.isBefore(now) && dateIssueEnd.isAfter(now);
}
// 발행 가능한지 수량 체크
public boolean availableIssueQuantity() {
// totalQuantity 가 null 이면 무제한 발급 가능하다는 것
if (totalQuantity == null) {
return true;
}
return totalQuantity > issuedQuantity;
}
// 발행
public void issue() {
if (!availableIssueQuantity()) {
throw new CouponIssueException(INVALID_COUPON_ISSUE_QUANTITY,
"발급 가능한 수량을 초과합니다. total : %s, issued : %s"
.formatted(totalQuantity, issuedQuantity));
}
if (!availableIssueDate()) {
throw new CouponIssueException(INVALID_COUPON_ISSUE_DATE,
"발급 가능한 일자가 아닙니다. request : %s, issueStart %s, issueEnd %s"
.formatted(LocalDateTime.now(), dateIssueStart, dateIssueEnd));
}
issuedQuantity++;
}
}
Coupon 도메인에서만 쓰는 Exception을 따로 만들어 주었다. 이처럼 각 도메인 별로 exception을 만들어줘도 되고, customException 하나로 모두 적용해도 된다. 자신의 취향에 따라 적용해보도록 하자.
- CouponIssueException
// mycouponcore/exception/CouponIssueException.java
package com.example.mycouponcore.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class CouponIssueException extends RuntimeException {
private final ErrorCode errorCode;
private final String message;
@Override
public String getMessage() {
return "[%s] %s".formatted(errorCode, message);
}
}
- ErrorCode
// mycouponcore/exception/ErrorCode.java
package com.example.mycouponcore.exception;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum ErrorCode {
INVALID_COUPON_ISSUE_QUANTITY("쿠폰 발급 수량이 유효하지 않습니다."),
INVALID_COUPON_ISSUE_DATE("쿠폰 발급 기간이 유효하지 않습니다."),
COUPON_NOT_EXIST("존재하지 않는 쿠폰입니다."),
DUPLICATED_COUPON_ISSUE("이미 발급된 쿠폰입니다."),
FAIL_COUPON_ISSUE_REQUEST("쿠폰 발급 요청에 실패하였습니다.");;
public final String message;
}
항상 메서드를 추가하면 어느 레이어든(Persistance Layer, Business Layer, Presentation Layer) 테스트코드를 작성하는 것이 좋다(TDD). 테스트 코드의 중요성에 대해서는 다음 글을 참고하자.
다음과 같은 방법으로 테스트 코드 작성 파일을 손쉽게 만들 수 있다.
원하는 클래스로 커서를 옮기고 Mac 의 경우 (Window 는 잘모르겠습니다 ㅠㅠ) Option+Enter
를 누르면 다음과 같은 팝업이 나오고, 저기서 Create Test 를 선택한다.
테스트하고자 하는 메서드를 클릭 후 OK 를 클릭하면 테스트 코드 작성 파일이 생성된다.
이제부터 만들었던 메서드들의 테스트 코드를 작성하여 테스트를 하면 된다.
테스트를 작성할 때는 크게 2가지를 고려하여 작성을 하면 된다.
- 성공 케이스
- 엣지 케이스
엣지 케이스의 경우 어떤 경계에 있는 상황에서 에러가 발생하거나 검증할 때 고려를 하는 것을 말한다. 엣지 케이스를 얼마나 잘 설계하고 테스트를 하느냐에 따라 서비스의 품질이 더 올라가니 엣지케이스를 잘 설계해보자.
그리고 테스트 코드도 하나의 문서이기에 누가 봐도 알아보기 쉽게 메서드 이름을 설정해야 한다.
만약 @DisplayName
을 통해 상황 설명이 명확하다면 굳이 메서드 이름까지 그 상황에 맞는 이름으로 하지 않아도 된다.
(상황을 설명할 때는 무엇을 했을 때 어떤 결과가 나온다는 것이 명확하게 제시해주는 것이 좋다.)
필자는 junit
보다는 assertj
가 더 메서드 체인이 직관적이라 테스트 때 사용했다. 이건 개발자의 취향에 따라 선택해서 사용하면 될 것 같다.
// mycouponcore/model/CouponTest.java
package com.example.mycouponcore.model;
import com.example.mycouponcore.exception.CouponIssueException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static com.example.mycouponcore.exception.ErrorCode.INVALID_COUPON_ISSUE_DATE;
import static com.example.mycouponcore.exception.ErrorCode.INVALID_COUPON_ISSUE_QUANTITY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CouponTest {
@DisplayName("발급 수량이 남아 있다면 true를 반환한다.")
@Test
void availableIssueQuantityWithSpareIssueQuantity() {
// given
Coupon coupon = Coupon.builder()
.totalQuantity(100)
.issuedQuantity(99)
.build();
// when
boolean result = coupon.availableIssueQuantity();
// then
assertThat(result).isTrue();
}
@DisplayName("발급 수량이 남아 있다면 false를 반환한다.")
@Test
void availableIssueQuantityWithNoSpareIssueQuantity() {
// given
Coupon coupon = Coupon.builder()
.totalQuantity(100)
.issuedQuantity(100)
.build();
// when
boolean result = coupon.availableIssueQuantity();
// then
assertThat(result).isFalse();
}
@DisplayName("최대 발급 수량이 설정 되어 있지 않다면 true를 반환한다.")
@Test
void availableIssueQuantityWithEmptyTotalQuantity() {
// given
Coupon coupon = Coupon.builder()
.issuedQuantity(100)
.build();
// when
boolean result = coupon.availableIssueQuantity();
// then
assertThat(result).isTrue();
}
@DisplayName("발급 기간이 시작되지 않았다면 false를 반환한다.")
@Test
void availableIssueDateWhenNotStartTheIssueDate() {
// given
Coupon coupon = Coupon.builder()
.dateIssueStart(LocalDateTime.now().plusDays(2))
.dateIssueEnd(LocalDateTime.now().plusDays(2))
.build();
// when
boolean result = coupon.availableIssueDate();
// then
assertThat(result).isFalse();
}
@DisplayName("발급 기간이 해당되면 true를 반환한다.")
@Test
void availableIssueDateWhenIncludeTheIssueDate() {
// given
Coupon coupon = Coupon
.builder()
.dateIssueStart(LocalDateTime.now().minusDays(1))
.dateIssueEnd((LocalDateTime.now().plusDays(2)))
.build();
// when
boolean result = coupon.availableIssueDate();
// then
assertThat(result).isTrue();
}
@DisplayName("발급 기간이 종료되었다면 false를 반환한다.")
@Test
void availableIssueDateWhenEndTheIssueDate() {
// given
Coupon coupon = Coupon
.builder()
.dateIssueStart(LocalDateTime.now().minusDays(1))
.dateIssueEnd((LocalDateTime.now().minusDays(2)))
.build();
// when
boolean result = coupon.availableIssueDate();
// then
assertThat(result).isFalse();
}
@DisplayName("발급 수량과 발급 기간이 유효하다면 발급에 성공한다.")
@Test
void issue_1() {
// given
Coupon coupon = Coupon
.builder()
.totalQuantity(100)
.issuedQuantity(99)
.dateIssueStart(LocalDateTime.now().minusDays(1))
.dateIssueEnd((LocalDateTime.now().plusDays(2)))
.build();
// when
coupon.issue();
// then
assertThat(coupon.getIssuedQuantity()).isEqualTo(100);
}
@DisplayName("발급 수량을 초과하면 예외를 반환한다.")
@Test
void issue_2() {
// given
Coupon coupon = Coupon
.builder()
.totalQuantity(100)
.issuedQuantity(100)
.dateIssueStart(LocalDateTime.now().minusDays(1))
.dateIssueEnd((LocalDateTime.now().plusDays(2)))
.build();
assertThatThrownBy(coupon::issue)
.isInstanceOf(CouponIssueException.class)
.extracting("errorCode")
.isEqualTo(INVALID_COUPON_ISSUE_QUANTITY);
}
@DisplayName("발급 기간이 아니면 예외를 반환한다.")
@Test
void issue_3() {
// given
Coupon coupon = Coupon
.builder()
.totalQuantity(100)
.issuedQuantity(99)
.dateIssueStart(LocalDateTime.now().plusDays(1))
.dateIssueEnd((LocalDateTime.now().plusDays(2)))
.build();
// when & then
assertThatThrownBy(coupon::issue)
.isInstanceOf(CouponIssueException.class)
.extracting("errorCode")
.isEqualTo(INVALID_COUPON_ISSUE_DATE);
}
}
availableIssueQuantity
와 availableIssueDate
메서드를 테스트 할떄는 메서드 이름까지 테스트 상황에 대해서 잘 명시했고, issue
메서드의 경우에는 _1, _2 이런식으로 테스트 케이스 네이밍을 했다.
어차피 @DisplayName
에서 상황과 결과를 명확히 제시해주기 때문에 테스트 메서드 이름까지 저렇게까지 할 필요는 없다고 생각한다.(필자 생각)
중요한건 테스트를 돌리는 입장에서 명확하게 어떤 테스트인줄 확인할 수 있는 정확한 지표가 하나라도 있어야 한다는 것이다.
쿠폰 발급 기능
이제 본격적으로 서비스를 만들어 보자.
먼저 QueryDsl 사용하기 위해 다음과 같은 config 파일을 추가해주자.
QueryDsl 은 동적 쿼리 작성에 아주 유용한 라이브러리다.
Jpa 만 쓴다면 쿼리를 쉽게 객체 지향적으로 쓸 수 있지만, 동적 조건에 대한 쿼리를 짜긴 쉽지 않다.
그럴때 바로 QueryDsl 쓴다면 쉽게 동적쿼리를 작성할 수 있다. 물론 처음에 설정이 까다로운데, 어느버전? 이상 부터는 설정도 쉽게 되는 것 같다?
- QueryDslConfig
// mycouponcore/configuration/QueryDslConfig.java
package com.example.mycouponcore.configuration;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class QueryDslConfig {
private final EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Q파일 생성을 위해 전체 빌드를 해준다.
다음과 같이 Q파일이 생성이 되면 QueryDsl 사용 준비가 된 것이다.
mysql 사용을 위해 repository 를 만들어 주자.
- CouponJpaRepository
// mycouponcore/repository/mysql/CouponJpaRepository.java
package com.example.mycouponcore.repository.mysql;
import com.example.mycouponcore.model.Coupon;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CouponJpaRepository extends JpaRepository<Coupon, Long> {
}
- CouponIssueJpaRepository
// mycouponcore/repository/mysql/CouponIssueJpaRepository.java
package com.example.mycouponcore.repository.mysql;
import com.example.mycouponcore.model.CouponIssue;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CouponIssueJpaRepository extends JpaRepository<CouponIssue, Long>, CouponIssueRepository {
}
- CouponIssueRepository
// mycouponcore/repository/mysql/CouponIssueRepository.java
package com.example.mycouponcore.repository.mysql;
import com.example.mycouponcore.model.CouponIssue;
public interface CouponIssueRepository {
CouponIssue findFirstCouponIssue(long couponId, long userId);
}
- CouponIssueRepositoryImpl
// mycouponcore/repository/mysql/CouponIssueRepositoryImpl.java
package com.example.mycouponcore.repository.mysql;
import com.example.mycouponcore.model.CouponIssue;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import static com.example.mycouponcore.model.QCouponIssue.couponIssue;
@Repository
@RequiredArgsConstructor
public class CouponIssueRepositoryImpl implements CouponIssueRepository{
private final JPAQueryFactory queryFactory;
@Override
public CouponIssue findFirstCouponIssue(long couponId, long userId) {
return queryFactory.selectFrom(couponIssue)
.where(couponIssue.couponId.eq(couponId)
.and(couponIssue.userId.eq(userId)))
.fetchFirst();
}
}
각 repository 안에 내용은 필요할 때 마다 정의해 줄 것이다. 그리고 QueryDsl repository는 따로 구현하고, CouponIssueJpaRepository
와 관련이 있기 때문에 CouponIssueRepository
상속시켜주었다.
이렇게 관련있는 repository는 묶어주는 것이 관리할때 편리하다.
이제부터 다음과 같은 기능을 가진 서비스를 구현하겠다.
- 쿠폰 발행(
issue
) - 쿠폰 발행 기록 저장(
saveCouponIssue
)
크게는 이 2가지 기능을 구현하겠지만, 각 기능 안에서도 필요한 메서드는 따로 더 추가했다.
이처럼 한 메서드에는 한가지 기능을 가지도록 분리하면 가독성에도 좋고, 나중에 유지보수 하는 측면에서도 좋다.(모듈화)
- CouponIssueService
// mycouponcore/service/CouponIssueService.java
package com.example.mycouponcore.service;
import com.example.mycouponcore.exception.CouponIssueException;
import com.example.mycouponcore.exception.ErrorCode;
import com.example.mycouponcore.model.Coupon;
import com.example.mycouponcore.model.CouponIssue;
import com.example.mycouponcore.repository.mysql.CouponIssueJpaRepository;
import com.example.mycouponcore.repository.mysql.CouponJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CouponIssueService {
private final CouponIssueJpaRepository couponIssueJpaRepository;
private final CouponJpaRepository couponJpaRepository;
@Transactional
public void issue(long couponId, long userId) {
Coupon coupon = findCoupon(couponId);
coupon.issue();
saveCouponIssue(couponId, userId);
}
public Coupon findCoupon(long couponId) {
return couponJpaRepository
.findById(couponId)
.orElseThrow(() -> {
throw new CouponIssueException(ErrorCode.COUPON_NOT_EXIST, "쿠폰 정책이 존재하지 않습니다. %s".formatted(couponId));
});
}
@Transactional
public CouponIssue saveCouponIssue(long couponId, long userId) {
// 방어 코드
checkAlreadyIssue(couponId, userId);
CouponIssue issue = CouponIssue
.builder()
.couponId(couponId)
.userId(userId)
.build();
return couponIssueJpaRepository.save(issue);
}
private void checkAlreadyIssue(long couponId, long userId) {
CouponIssue issue = couponIssueJpaRepository.findFirstCouponIssue(couponId, userId);
if (issue != null) {
throw new CouponIssueException(ErrorCode.DUPLICATED_COUPON_ISSUE, "이미 발급된 쿠폰입니다. user_id: %s, coupon_id: %s".formatted(userId, couponId));
}
}
}
이렇게 서비스를 정의했다면 바로 테스트 코드를 작성해주자.
앞서 Persistance Layer 를 테스트 코드 작성한 것과는 다르게 Business Layer 부터는 관련 로직을 수행하기 위해서는 등록해둔 빈 정보가 필요하다.
// mycouponcore/service/CouponIssueServiceTest.java
package com.example.mycouponcore.service;
import com.example.mycouponcore.TestConfig;
import com.example.mycouponcore.exception.CouponIssueException;
import com.example.mycouponcore.exception.ErrorCode;
import com.example.mycouponcore.model.Coupon;
import com.example.mycouponcore.model.CouponIssue;
import com.example.mycouponcore.model.CouponType;
import com.example.mycouponcore.repository.mysql.CouponIssueJpaRepository;
import com.example.mycouponcore.repository.mysql.CouponJpaRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import static com.example.mycouponcore.exception.ErrorCode.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CouponIssueServiceTest extends TestConfig {
@Autowired
CouponIssueService sut;
@Autowired
CouponIssueJpaRepository couponIssueJpaRepository;
@Autowired
CouponJpaRepository couponJpaRepository;
@BeforeEach
void setUp() {
// init data set(수정해도 모든 테스트에 영향을 주지 않는다면)
// 테스트 실행전에 config 설정 or 초기 데이터 셋팅에 주로 사용
}
@AfterEach
void tearDown() {
// data cleaning
couponJpaRepository.deleteAllInBatch();
couponIssueJpaRepository.deleteAllInBatch();
}
@DisplayName("발급 수량, 발급 기한, 중복 발급 문제 없다면 쿠폰을 발급한다.")
@Test
void issue_1() {
// given
long userId = 1;
Coupon coupon = Coupon.builder()
.couponType(CouponType.FIRST_COME_FIRST_SERVED)
.title("선착순 테스트 쿠폰")
.totalQuantity(100)
.issuedQuantity(0)
.dateIssueStart(LocalDateTime.now().minusDays(1))
.dateIssueEnd(LocalDateTime.now().plusDays(1))
.build();
couponJpaRepository.save(coupon);
// when
sut.issue(coupon.getId(), userId);
// then
Coupon couponResult = couponJpaRepository
.findById(coupon.getId())
.orElseThrow(() -> {
throw new CouponIssueException(ErrorCode.COUPON_NOT_EXIST, "존재하지 않는 쿠폰입니다.");
});
assertThat(couponResult.getIssuedQuantity()).isEqualTo(1);
CouponIssue couponIssueResult = couponIssueJpaRepository.findFirstCouponIssue(coupon.getId(), userId);
assertThat(couponIssueResult).isNotNull();
}
@DisplayName("발급 수량이 문제가 있으면 예외가 발생한다.")
@Test
void issue_2() {
// given
long userId = 1;
Coupon coupon = Coupon.builder()
.couponType(CouponType.FIRST_COME_FIRST_SERVED)
.title("선착순 테스트 쿠폰")
.totalQuantity(100)
.issuedQuantity(100)
.dateIssueStart(LocalDateTime.now().minusDays(1))
.dateIssueEnd(LocalDateTime.now().plusDays(1))
.build();
couponJpaRepository.save(coupon);
// when && then
assertThatThrownBy(() -> sut.issue(coupon.getId(), userId))
.isInstanceOf(CouponIssueException.class)
.extracting("ErrorCode")
.isEqualTo(INVALID_COUPON_ISSUE_QUANTITY);
}
@DisplayName("발급 기한에 문제가 있으면 예외가 발생한다.")
@Test
void issue_3() {
// given
long userId = 1;
Coupon coupon = Coupon.builder()
.couponType(CouponType.FIRST_COME_FIRST_SERVED)
.title("선착순 테스트 쿠폰")
.totalQuantity(100)
.issuedQuantity(0)
.dateIssueStart(LocalDateTime.now().plusDays(2))
.dateIssueEnd(LocalDateTime.now().plusDays(1))
.build();
couponJpaRepository.save(coupon);
// when && then
assertThatThrownBy(() -> sut.issue(coupon.getId(), userId))
.isInstanceOf(CouponIssueException.class)
.extracting("ErrorCode")
.isEqualTo(INVALID_COUPON_ISSUE_DATE);
}
@DisplayName("쿠폰 중복 발급 검증에 문제가 예외가 발생한다.")
@Test
void issue_4() {
// given
long userId = 1;
Coupon coupon = Coupon.builder()
.couponType(CouponType.FIRST_COME_FIRST_SERVED)
.title("선착순 테스트 쿠폰")
.totalQuantity(100)
.issuedQuantity(0)
.dateIssueStart(LocalDateTime.now().minusDays(2))
.dateIssueEnd(LocalDateTime.now().plusDays(1))
.build();
couponJpaRepository.save(coupon);
CouponIssue couponIssue = CouponIssue
.builder()
.userId(userId)
.couponId(coupon.getId())
.build();
couponIssueJpaRepository.save(couponIssue);
// when && then
assertThatThrownBy(() -> sut.issue(coupon.getId(), userId))
.isInstanceOf(CouponIssueException.class)
.extracting("ErrorCode")
.isEqualTo(DUPLICATED_COUPON_ISSUE);
}
@DisplayName("쿠폰이 존재하지 않는다면 예외가 발생한다.")
@Test
void issue_5() {
// given
long userId = 1;
long couponId = 1;
// when && then
assertThatThrownBy(() -> sut.issue(couponId, userId))
.isInstanceOf(CouponIssueException.class)
.extracting("ErrorCode")
.isEqualTo(COUPON_NOT_EXIST);
}
@DisplayName("쿠폰이 존재하면 쿠폰을 조회한다.")
@Test
void findCoupon_1() {
// given
Coupon coupon = Coupon.builder()
.couponType(CouponType.FIRST_COME_FIRST_SERVED)
.title("선착순 테스트 쿠폰")
.totalQuantity(100)
.issuedQuantity(0)
.dateIssueStart(LocalDateTime.now().minusDays(2))
.dateIssueEnd(LocalDateTime.now().plusDays(1))
.build();
couponJpaRepository.save(coupon);
// when
Coupon result = sut.findCoupon(coupon.getId());
// then
assertThat(result).isNotNull();
}
@DisplayName("쿠폰이 존재하지 않으면 예외가 발생한다.")
@Test
void findCoupon_2() {
// given
long couponId = 1;
// when && then
assertThatThrownBy(() -> sut.findCoupon(couponId))
.isInstanceOf(CouponIssueException.class)
.extracting("errorCode")
.isEqualTo(COUPON_NOT_EXIST);
}
@DisplayName("다른 쿠폰을 조회하면 예외가 발생한다.")
@Test
void findCoupon_3() {
// given
long couponId = 100;
Coupon coupon = Coupon.builder()
.couponType(CouponType.FIRST_COME_FIRST_SERVED)
.title("선착순 테스트 쿠폰")
.totalQuantity(100)
.issuedQuantity(0)
.dateIssueStart(LocalDateTime.now().minusDays(2))
.dateIssueEnd(LocalDateTime.now().plusDays(1))
.build();
couponJpaRepository.save(coupon);
// when && then
assertThatThrownBy(() -> sut.findCoupon(couponId))
.isInstanceOf(CouponIssueException.class)
.extracting("errorCode")
.isEqualTo(COUPON_NOT_EXIST);
}
@DisplayName("쿠폰 발급 내역이 존재하면 예외가 발생한다.")
@Test
void saveCouponIssue_1() {
// given
long userId = 1;
long couponId= 1;
CouponIssue couponIssue = CouponIssue
.builder()
.couponId(couponId)
.userId(userId)
.build();
couponIssueJpaRepository.save(couponIssue);
// when && then
assertThatThrownBy(() -> sut.saveCouponIssue(couponId, userId))
.isInstanceOf(CouponIssueException.class)
.extracting("errorCode")
.isEqualTo(DUPLICATED_COUPON_ISSUE);
}
@DisplayName("쿠폰 발급 내역이 존재하지 않으면 쿠폰을 발급한다.")
@Test
void saveCouponIssue_2() {
// given
long userId = 1;
long couponId= 1;
// when
sut.saveCouponIssue(couponId, userId);
// then
CouponIssue result = couponIssueJpaRepository.findFirstCouponIssue(couponId, userId);
assertThat(result).isNotNull();
}
}
이어서 Mysql 기반 선착순 쿠폰 발급 기능 개발 (3)에서 계속 포스팅 하겠다.
'토이프로젝트 > 선착순 이벤트 쿠폰 시스템' 카테고리의 다른 글
Mysql 기반 선착순 쿠폰 발급 기능 개발 (4) (0) | 2024.04.30 |
---|---|
Mysql 기반 선착순 쿠폰 발급 기능 개발 (3) (0) | 2024.04.18 |
Mysql 기반 선착순 쿠폰 발급 기능 개발 (1) (0) | 2024.04.08 |
2. 프로젝트 환경 설정 (0) | 2024.04.06 |
1. 요구사항 분석 및 도메인 설계 (0) | 2024.04.02 |