토이프로젝트/선착순 이벤트 쿠폰 시스템

Mysql 기반 선착순 쿠폰 발급 기능 개발 (2)

feel2 2024. 4. 16. 19:08
반응형

쿠폰 엔터티 발급 기능

이제부터 본격적으로 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). 테스트 코드의 중요성에 대해서는 다음 글을 참고하자.

https://feel2.tistory.com/30

 

TestCode

https://www.slideshare.net/slideshow/testcode/266737205 TestCode TestCode - Download as a PDF or view online for free www.slideshare.net

feel2.tistory.com

다음과 같은 방법으로 테스트 코드 작성 파일을 손쉽게 만들 수 있다.

 

원하는 클래스로 커서를 옮기고 Mac 의 경우 (Window 는 잘모르겠습니다 ㅠㅠ) Option+Enter 를 누르면 다음과 같은 팝업이 나오고, 저기서 Create Test 를 선택한다.

 

테스트하고자 하는 메서드를 클릭 후 OK 를 클릭하면 테스트 코드 작성 파일이 생성된다.

 

이제부터 만들었던 메서드들의 테스트 코드를 작성하여 테스트를 하면 된다.

 

테스트를 작성할 때는 크게 2가지를 고려하여 작성을 하면 된다.

  1. 성공 케이스
  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);
    }

}

 

availableIssueQuantityavailableIssueDate 메서드를 테스트 할떄는 메서드 이름까지 테스트 상황에 대해서 잘 명시했고, 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)에서 계속 포스팅 하겠다.

반응형