스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초

스프링 부트 핵심 가이드(장정우 지음) - 테스트 코드 작성하기2

개발학생 2025. 8. 8. 17:24
반응형

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성  

 

[Java] 차근차근 Java 설치하기 (JDK17, Window 11)

자바 개발 도구 설치 방법에 대해서 알아보겠습니다. Java17은 LTS(Long Term Support : 장기 지원) 릴리즈로 1년 후까지 기술 지원 및 버그를 개선한 서비스를 제공받을 수 있습니다. 업데이트 버전을 꾸

yungenie.tistory.com

 

[Windows] Spring Tool Suite 4(STS 4) 다운로드 및 설치

STS란?Spring Tool Suite(STS)는 스프링 프로젝트를 생성하고, 개발할 수 있게 해주는 도구입니다. STS 설치 과정에 대해 설명드리겠습니다. 설치 파일 다운로드STS 공식 사이트에서 설치 파일을 다운로

priming.tistory.com

 

MySQL :: Download MySQL Community Server

Select Version: 9.3.0 Innovation 8.4.5 LTS 8.0.42 Select Operating System: Select Operating System… Microsoft Windows Ubuntu Linux Debian Linux SUSE Linux Enterprise Server Red Hat Enterprise Linux / Oracle Linux Fedora Linux - Generic Oracle Solaris mac

dev.mysql.com

  • Gradle

**STS에서 Gradle 프로젝트 생성한 과정 

*** 함께 보면 좋은 글

2025.07.10 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요

 

스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요

1. 스프링 프레임워크자바(Java) 기반 애플리케이션 프레임워크로, 엔터프라이즈급(기업 환경 대상 개발) 애플리케이션을 위한 다양한 기능 제공-> 오픈소스 경량급 애플리케이션 프레임워크로

keep-programming-study.tistory.com

2025.07.11 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식

 

스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식

1. 서버 간 통신마이크로서비스 아키텍처에서 한 서버가 다른 서버에 통신을 요청하는 것을 의미-> 한 대는 서버/다른 한 대는 클라이언트가 됨 가장 많이 사용되는 방식은 HTTP/HTTPS 방식(TCP/IP, SOA

keep-programming-study.tistory.com

2025.07.16 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - REST API 명세를 문서화하는 방법(Swagger), 로깅 라이브러리(Logback)

 

스프링 부트 핵심 가이드(장정우 지음) - REST API 명세를 문서화하는 방법(Swagger), 로깅 라이브러리

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법

keep-programming-study.tistory.com

2025.07.29 - [스프링(Spring), 스프링부트(SpringBoot)/스프링부트(SpringBoot) 기초] - 스프링 부트 핵심 가이드(장정우 지음) - 테스트 코드 작성하기

 

스프링 부트 핵심 가이드(장정우 지음) - 테스트 코드 작성하기

*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법

keep-programming-study.tistory.com

 

4. JUnit을 활용한 테스트 코드 작성

1) JUnit의 세부 모듈

JUnit5의 모듈 구조

(1) JUnit Platform

  • JVM에서 테스트를 시작하기 위한 뼈대 역할
  • 테스트를 발견하고 테스트 계획을 생성하는 테스트 엔진(TestEngine)의 인터페이스를 갖고 있음
    -> 테스트 발견/수행/결과 보고 역할, 각종 IDE와의 연동을 보조하는 역할
  • TestEngine API, Console Launcher, JUnit 4 Based Runner 등이 포함되어 있음

(2) JUnit Jupiter

  • 테스트 엔진 API의 구현체를 포함하고 있음 
  • JUnit5에서 제공하는 Jupiter 기반 테스트를 실행하기 위한 테스트 엔진을 갖고 있음
  • 별도 모듈의 역할을 수행하는 테스트의 실제 구현체 중 하나가 Jupiter Engine
    -> Jupiter API를 활용해서 작성한 테스트 코드를 발견하고 실행하는 역할을 수행 

(3) JUnit Vintage

  • JUnit3, 4에 대한 테스트 엔진 API를구현
  • 기존에 작성된 JUnit3, 4 버전의 테스트 코드를 실행할 때 사용
  • Vintage Engine을 포함 

2) 스프링 부트 프로젝트에 JUnit 테스트 준비

  • DAO 레이어는 제외하고, 서비스 레이어에서 바로 리포지토리를 사용하는 구조로 수정

(1) build.gradle에 의존성 추가

depenedencies {
  ... 생략 ...
  // 테스트 코드를 작성할 때 필요한 라이브러리들을 포함한 Spring Boot 테스트 스타터 의존성
  testImplementation 'org.springframework.boot:spring-boot-starter-test'

  // 테스트 실행 시 필요한 JUnit 플랫폼 런처 (테스트를 실제로 실행하는 데 사용됨)
  testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
  // Mockito의 핵심 라이브러리: mock 객체를 생성하고 동작을 정의할 수 있게 해줌
  testImplementation 'org.mockito:mockito-core:5.2.0'
  // JUnit 5(Jupiter)와 Mockito를 함께 사용할 수 있도록 해주는 확장 라이브러리
  //    @ExtendWith(MockitoExtension.class) 등을 사용할 때 필요함
  testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0'
  // Gson 라이브러리 사용 - Google에서 제공하는 JSON 직렬화/역직렬화 라이브러리로, Java 객체를 JSON으로 변환하거나 JSON을 Java 객체로 변환할 때 사용
  implementation 'com.google.code.gson:gson:2.10.1'
  // H2는 인메모리 관계형 데이터베이스로, 테스트 환경에서만 사용    
  testImplementation 'com.h2database:h2'
  // AssertJ 사용
  testImplementation 'org.assertj:assertj-core:3.24.2'
}
  • spring-boot-starter-test 라이브러리에서 제공하는 라이브러리들 
    • 단정문 (assert): "이 조건이 반드시 참이어야 해!" 라고 코드에 명시적으로 선언하는 문장
      -> 조건이 거짓이면, 프로그램이 오류를 발생시키고 멈춤 
JUnit 5 자바 애플리케이션의 단위 테스트 지원
Spring Test & Spring Boot Test 스프링 부트 애플리케이션에 대한 유틸리티와 통합 테스트 지원
AssertJ 다양한 단정문(assert)을 지원
Hamcrest Matcher를 지원
Mockito 자바 Mock 객체 지원
JSONassert JSON용 단정문(assert) 라이브러리
JsonPath JSON용 XPath 지원 

(2) build.gradle 우클릭 후 Gradle -> Refresh Gradle Project 클릭

build.gradle 우클릭 후 Gradle -> Refresh Gradle Project 클릭

(3) ProductServiceImpl.java 수정

// ProductServiceImpl.java
package com.example.demo.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.jpa.data.dto.ProductDTO;
import com.example.demo.jpa.data.dto.ProductResponseDTO;
import com.example.demo.jpa.data.entity.Product;
import com.example.demo.jpa.data.repository.ProductRepository;
import com.example.demo.service.ProductService;

/**
 * ProductService 인터페이스의 구현 클래스
 * 비즈니스 로직을 처리하며, Repository를 통해 DB와 통신하고 DTO를 통해 Controller와 데이터를 주고받음
 */
@Service
public class ProductServiceImpl implements ProductService {

	private final Logger LOGGER = LoggerFactory.getLogger(ProductServiceImpl.class);
	private final ProductRepository productRepository;
	
    /**
     * 생성자 주입 방식으로 ProductRepository를 주입
     * @param productRepository 상품 데이터 접근 객체
     */
    @Autowired
    public ProductServiceImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    /**
     * 상품 번호를 기반으로 상품 정보를 조회하고, 응답 DTO로 변환하여 반환
     * @param number 조회할 상품의 고유 번호
     * @return ProductResponseDTO 조회된 상품 정보
     */
    @Override
    public ProductResponseDTO getProduct(Long number) {
        LOGGER.info("[getProduct] input number : {}", number);
    	Product product = productRepository.findById(number).get();
        
        LOGGER.info("[getProduct] product number : {}, name : {}", product.getNumber(), product.getName());

        ProductResponseDTO productResponseDTO = new ProductResponseDTO();
        productResponseDTO.setNumber(product.getNumber());
        productResponseDTO.setName(product.getName());
        productResponseDTO.setPrice(product.getPrice());
        productResponseDTO.setStock(product.getStock());

        return productResponseDTO;
    }

    /**
     * 클라이언트로부터 전달받은 DTO를 기반으로 상품을 생성하고 저장
     * 저장된 상품 정보를 응답 DTO로 변환하여 반환
     * @param productDTO 저장할 상품 정보
     * @return ProductResponseDTO 저장된 상품 정보
     */
    @Override
    public ProductResponseDTO saveProduct(ProductDTO productDTO) {
    	LOGGER.info("[saveProduct] productDTO : {}", productDTO.toString());
        Product product = new Product();
        product.setName(productDTO.getName());
        product.setPrice(productDTO.getPrice());
        product.setStock(productDTO.getStock());

        Product savedProduct = productRepository.save(product);
        LOGGER.info("[saveProduct] saveProduct : {}", savedProduct);

        ProductResponseDTO productResponseDTO = new ProductResponseDTO();
        productResponseDTO.setNumber(savedProduct.getNumber());
        productResponseDTO.setName(savedProduct.getName());
        productResponseDTO.setPrice(savedProduct.getPrice());
        productResponseDTO.setStock(savedProduct.getStock());
        
        return productResponseDTO;
    }

    /**
     * 특정 상품의 이름을 변경하고, 변경된 정보를 응답 DTO로 반환
     * @param number 변경할 상품의 고유 번호
     * @param name 새로운 상품 이름
     * @return ProductResponseDTO 변경된 상품 정보
     * @throws Exception 상품이 존재하지 않거나 변경 실패 시 예외 발생
     */
    @Override
    public ProductResponseDTO changeProductName(Long number, String name) throws Exception {
    	Product foundroduct = productRepository.findById(number).get();
    	foundroduct.setName(name);
    	Product changedProduct = productRepository.save(foundroduct);

        ProductResponseDTO productResponseDTO = new ProductResponseDTO();
        productResponseDTO.setNumber(changedProduct.getNumber());
        productResponseDTO.setName(changedProduct.getName());
        productResponseDTO.setPrice(changedProduct.getPrice());
        productResponseDTO.setStock(changedProduct.getStock());

        return productResponseDTO;
    }

    /**
     * 특정 상품을 삭제
     * @param number 삭제할 상품의 고유 번호
     * @throws Exception 상품이 존재하지 않거나 삭제 실패 시 예외 발생
     */
    @Override
    public void deleteProduct(Long number) throws Exception {
        productRepository.deleteById(number);
    }
}

(4) Product.java(엔티티 클래스)에 어노테이션 추가 

// Product.java
package com.example.demo.jpa.data.entity;

import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

//클래스의 모든 필드에 대해 Getter 메서드를 자동 생성
@Getter
//클래스의 모든 필드에 대해 Setter 메서드를 자동 생성
@Setter
//파라미터가 없는 기본 생성자를 자동 생성
@NoArgsConstructor
//모든 필드를 파라미터로 받는 생성자를 자동 생성
@AllArgsConstructor
//이 클래스는 JPA 엔티티로 데이터베이스 테이블과 매핑
@Entity
// 객체 생성 시 빌더 패턴을 사용할 수 있게 해주는 어노테이션
@Builder
// equals()와 hashCode() 메서드를 자동 생성해주는 어노테이션
@EqualsAndHashCode
// toString() 메서드를 자동 생성하되, name 필드는 출력에서 제외
@ToString(exclude="name")
//엔티티가 매핑될 테이블 이름을 명시적으로 지정 (기본값은 클래스 이름)
@Table(name="product")
public class Product {
	// 기본 키(PK)로 지정된 필드. 데이터베이스의 고유 식별자 역할
    @Id
    // ID 값을 자동 생성함. GenerationType.IDENTITY는 DB가 직접 AUTO_INCREMENT 방식으로 생성하도록 함(insert 후에 pk 자동생성)
    // -> IDENTITY외에도 AUTO(기본값, 사용하는 데이터베이스에 맞게 기본값 자동 생성), 
    //    SEQUENCE(Oracle, PostgreSQL 등 시퀀스를 지원하는 DB에서 별도 시퀀스 객체를 생성해 PK값 증가), 
    //    TABLE(키 값을 관리하는 별도 테이블을 생성해 PK값 증가-성능 낮음)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
	private long number;

    // 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑되므로, 별다른 설정을 하지 않을 경우 @Column을 생략해도 됨
    // 상품 이름 (Null 허용하지 않음)
    @Column(nullable = false)
    private String name;
    
    // 가격 (Null 허용하지 않음)
    @Column(nullable = false)
    private int price;
    
    // 재고 수량 (Null 허용하지 않음)
    @Column(nullable = false)
    private int stock;

    // 상품 생성 일시 (자동 설정할 수 있으나 현재는 수동 할당 전제)
    private LocalDateTime createdAt;

    // 상품 수정 일시
    private LocalDateTime updatedAt;
}

3) 스프링 부트 프로젝트에 JUnit 테스트 실행 전, JUnit의 생명주기 알아보기 

(1) 생명주기와 관련되며, 테스트 순서에 관여하는 어노테이션들

@Test 테스트 코드를 포함한 메서드 정의
@BeforeAll 테스트를 시작하기 전에 호출되는 메서드 정의
@BeforeEach 각 테스트 메서드가 실행되기 전에 동작하는 메서드 정의
@AfterAll 테스트를 종료하면서 호출되는 메서드 정의
@AfterEach 각 테스트 메서드가 종료되면서 호출되는 메서드 정의 

(2) 위 어노테이션들의 동작 알아보기 

  • src/test/java 경로의 패키지에서 테스트 코드 작성할 클래스 생성
// src/test/java/com.example.demo/TestLifeCycle.java
package com.example.demo;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class TestLifeCycle {
	@BeforeAll
	static void beforeAll() {
		System.out.println("## BeforeAll Annotation 호출");
		System.out.println();
	}
	
	@AfterAll
	static void afterAll() {
		System.out.println("## AfterAll Annotation 호출");
		System.out.println();
	}
	
	@BeforeEach
	void beforeEach() {
		System.out.println("## BeforeEach Annotation 호출");
		System.out.println();
	}
	
	@AfterEach
	void afterEach() {
		System.out.println("## AfterEach Annotation 호출");
		System.out.println();
	}
	
	@Test
	void test1() {
		System.out.println("## test1 시작");
		System.out.println();
	}
	
	@Test
	@DisplayName("Test Case2")
	void test2() {
		System.out.println("## test2 시작");
		System.out.println();
	}
	
	@Test
	@Disabled
	void test3() {
		System.out.println("## test3 시작");
		System.out.println();
	}
}
  • 테스트 코드 실행

해당 파일 우클릭 -> Run As -> JUnit Test를 클릭해서 실행

  • 테스트 코드 실행 결과: @Disabled 어노테이션을 지정한 test3() 메서드를 제외하고 모든 메서드가 실행됨  

JUnit 단위 테스트 실행 결과

4) 스프링 부트 프로젝트에 JUnit 테스트 실행

*** 책 내용대로 @RequsetBody을 사용한 것이 아닌, @RequestParam을 사용했으므로 테스트가 되지 않는 것이 정상.. 

  • src/test/java 경로의 패키지에서 테스트 코드 작성할 클래스 생성
// src/test/java/com.example.demo/ProductControllerTest.java
package com.example.demo;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import static org.mockito.Mockito.verify; // 호출 여부 검증
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; // HTTP 요청 생성
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; // 응답 검증
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; // 결과 출력
import static org.mockito.BDDMockito.given; // BDD 스타일의 mock 설정
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; // Controller 테스트용 어노테이션
import org.springframework.test.web.servlet.MockMvc; // 가짜 HTTP 요청을 보내는 객체

import com.example.demo.controller.ProductController;
import com.example.demo.jpa.data.dto.ProductDTO;
import com.example.demo.jpa.data.dto.ProductResponseDTO;
import com.example.demo.service.impl.ProductServiceImpl;

@WebMvcTest(ProductController.class) // ProductController만 테스트 대상으로 설정
public class ProductControllerTest {
    
    @Autowired
    private MockMvc mockMvc; // 실제 서버 없이 Controller를 테스트할 수 있게 해주는 객체
    
    @Mock
    ProductServiceImpl productService; // ProductServiceImpl을 mock 객체로 생성
    
    @Test
    @DisplayName("MockMvc를 통한 Product 데이터 가져오기 테스트") // 테스트 이름 지정
    void getProductTest() throws Exception {
        
        // productService.getProduct(123L)가 호출되면, 아래 DTO를 반환하도록 설정
        given(productService.getProduct(123L)).willReturn(
                new ProductResponseDTO(123L, "pen", 5000, 2000));
        
        String productId = "123";
        
        // GET 요청을 /product?number=123 으로 보내고, 응답을 검증
        mockMvc.perform(
                    get("/product?number=" + productId)) // 요청 생성
                .andExpect(status().isOk()) // HTTP 상태 코드 200 확인
                .andExpect(jsonPath("$.number").exists()) // JSON 응답에 number 필드 존재 확인
                .andExpect(jsonPath("$.name").exists())   // name 필드 존재 확인
                .andExpect(jsonPath("$.price").exists())  // price 필드 존재 확인
                .andExpect(jsonPath("$.number").exists()) // number 필드 중복 확인 (한 번만 해도 충분)
                .andDo(print()); // 응답 결과 콘솔에 출력
        
        // productService.getProduct(123L)가 실제로 호출되었는지 검증
        verify(productService).getProduct(123L);
    }
    
    @Test
    @DisplayName("상품 생성 API 테스트 - createProduct")
    void createProductTest() throws Exception {
        // productService.saveProduct(...)가 호출되면 아래 DTO를 반환하도록 설정
        ProductResponseDTO mockResponse = new ProductResponseDTO(101L, "notebook", 15000, 500);
        ProductDTO inputDTO = new ProductDTO();
        inputDTO.setName("notebook");
        inputDTO.setPrice(15000);
        inputDTO.setStock(500);

        given(productService.saveProduct(inputDTO)).willReturn(mockResponse);

        // POST 요청을 /product로 보내고, 응답을 검증
        mockMvc.perform(
                post("/product")
                    .param("name", "notebook")
                    .param("price", "15000")
                    .param("stock", "500"))
            .andExpect(status().isOk()) // HTTP 200 응답 확인
            .andExpect(jsonPath("$.number").value(101)) // 응답 JSON의 number 필드 값 확인
            .andExpect(jsonPath("$.name").value("notebook")) // name 필드 값 확인
            .andExpect(jsonPath("$.price").value(15000)) // price 필드 값 확인
            .andExpect(jsonPath("$.stock").value(500)) // stock 필드 값 확인
            .andDo(print()); // 결과 출력

        // productService.saveProduct(...)가 호출되었는지 검증
        verify(productService).saveProduct(inputDTO);
    }
}
// src/test/java/com.example.demo/ProductServiceTest.java
package com.example.demo;

// Mockito와 JUnit에서 사용하는 static 메서드들을 import
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.AdditionalAnswers.returnsFirstArg;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

// 테스트 대상 클래스와 관련된 DTO, Entity, Repository 클래스 import
import com.example.demo.jpa.data.dto.ProductDTO;
import com.example.demo.jpa.data.dto.ProductResponseDTO;
import com.example.demo.jpa.data.entity.Product;
import com.example.demo.jpa.data.repository.ProductRepository;
import com.example.demo.service.impl.ProductServiceImpl;

// ProductServiceImpl 클래스에 대한 단위 테스트 클래스 정의
public class ProductServiceTest {
    // ProductRepository를 Mockito를 사용해 Mock 객체로 생성
    private ProductRepository productRepository = Mockito.mock(ProductRepository.class);
    // 테스트 대상인 ProductServiceImpl 인스턴스 선언
    private ProductServiceImpl productService;
    
    // 각 테스트 실행 전에 호출되는 초기화 메서드
    @BeforeEach
    public void setUpTest() {
        // ProductServiceImpl 인스턴스를 생성하면서 Mock Repository를 주입
        productService = new ProductServiceImpl(productRepository);
    }
    
    // saveProduct 메서드에 대한 단위 테스트
    @Test
    void saveProductTest() {
        // Repository의 save 메서드가 호출되면 전달된 인자를 그대로 반환하도록 설정
        Mockito.when(productRepository.save(any(Product.class)))
        .then(returnsFirstArg());
        
        // 테스트 대상 메서드 호출: ProductDTO를 전달하여 저장 요청
        ProductResponseDTO productResponseDTO = productService.saveProduct(
                new ProductDTO("펜", 1000, 1234));
        
        // 반환된 결과가 기대한 값과 일치하는지 검증
        Assertions.assertEquals(productResponseDTO.getName(), "펜");
        Assertions.assertEquals(productResponseDTO.getPrice(), 1000);
        Assertions.assertEquals(productResponseDTO.getStock(), 1234);
        
        // Repository의 save 메서드가 실제로 호출되었는지 검증
        verify(productRepository).save(any());
    }
}

 

// src/test/java/com.example.demo/ProductRepositoryTest.java
package com.example.demo;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

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

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import com.example.demo.jpa.data.entity.Product;
import com.example.demo.jpa.data.repository.ProductRepository;

// JPA 관련 테스트를 위한 설정. 실제 DB 대신 테스트용 인메모리 DB를 사용함
@DataJpaTest
public class ProductRepositoryTest {

    // 테스트 대상인 ProductRepository를 주입받음
    @Autowired
    private ProductRepository productRepository;
    
    @Test
    void saveTest() {
        // given: 저장할 Product 객체 생성
        Product product = new Product();
        product.setName("펜");
        product.setPrice(1000);
        product.setStock(1000);
        
        // when: Product 저장
        Product saveProduct = productRepository.save(product);
        
        // then: 저장된 값이 원래 값과 같은지 검증
        assertEquals(product.getName(), saveProduct.getName());
        assertEquals(product.getPrice(), saveProduct.getPrice());
        assertEquals(product.getStock(), saveProduct.getStock());
    }
    
    @Test
    void selectTest() {
        // given: 저장할 Product 객체 생성 및 저장
        Product product = new Product();
        product.setName("펜");
        product.setPrice(1000);
        product.setStock(1000);
        
        Product savedProduct = productRepository.saveAndFlush(product);
        
        // when: 저장된 Product를 ID로 조회
        Product foundProduct = productRepository.findById(savedProduct.getNumber()).get();
        
        // then: 조회된 값이 원래 값과 같은지 검증
        assertEquals(product.getName(), foundProduct.getName());
        assertEquals(product.getPrice(), foundProduct.getPrice());
        assertEquals(product.getStock(), foundProduct.getStock());
    }
    
    @Test
    public void basicCRUDTest() {
        // Create 테스트
        // given: Product 객체 생성
        Product givenProduct = Product.builder()
                .name("노트")
                .price(1000)
                .stock(500)
                .build();
        
        // when: Product 저장
        Product savedProduct = productRepository.save(givenProduct);
        
        // then: 저장된 값이 원래 값과 같은지 검증
        Assertions.assertThat(savedProduct.getNumber())
            .isEqualTo(givenProduct.getNumber());
        Assertions.assertThat(savedProduct.getName())
            .isEqualTo(givenProduct.getName());
        Assertions.assertThat(savedProduct.getPrice())
            .isEqualTo(givenProduct.getPrice());
        Assertions.assertThat(savedProduct.getStock())
            .isEqualTo(givenProduct.getStock());
        
        // Read 테스트
        // when: 저장된 Product를 ID로 조회
        Product selectedProduct = productRepository.findById(savedProduct.getNumber())
                .orElseThrow(RuntimeException::new);
        
        // then: 조회된 값이 원래 값과 같은지 검증
        Assertions.assertThat(selectedProduct.getNumber())
            .isEqualTo(givenProduct.getNumber());
        Assertions.assertThat(selectedProduct.getName())
            .isEqualTo(givenProduct.getName());
        Assertions.assertThat(selectedProduct.getPrice())
            .isEqualTo(givenProduct.getPrice());
        Assertions.assertThat(selectedProduct.getStock())
            .isEqualTo(givenProduct.getStock());
        
        // Update 테스트
        // when: Product 이름 변경
        Product foundProduct = productRepository.findById(selectedProduct.getNumber())
                .orElseThrow(RuntimeException::new);
        
        foundProduct.setName("장난감");
        
        // 변경된 Product 다시 조회
        Product updatedProduct = productRepository.findById(selectedProduct.getNumber())
                .orElseThrow(RuntimeException::new);
        
        // then: 이름이 변경되었는지 확인
        assertEquals(updatedProduct.getName(), "장난감");
        
        // Delete 테스트
        // when: Product 삭제
        productRepository.delete(updatedProduct);
        
        // then: 삭제된 Product가 존재하지 않는지 확인
        assertFalse(productRepository.findById(selectedProduct.getNumber()).isPresent());
    }
}

 

반응형