package codej.todo_list.demo.todo.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.GenericGenerator;
import java.time.LocalDateTime;
@Entity
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long pno;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
private LocalDateTime createDate;
private LocalDateTime updateDate;
}
package codej.todo_list.demo.todo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductDto {
private String name;
private int price;
private int package codej.todo_list.demo.todo.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class ProductResponseDto {
private Long pno;
private String name;
private int price;
private int stock;
public ProductResponseDto(Long pno, String name, int price, int stock) {
this.pno = pno;
this.name = name;
this.price = price;
this.stock = stock;
}
}
stock;
}
package codej.todo_list.demo.todo.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class ProductResponseDto {
private Long pno;
private String name;
private int price;
private int stock;
public ProductResponseDto(Long pno, String name, int price, int stock) {
this.pno = pno;
this.name = name;
this.price = price;
this.stock = stock;
}
}
package codej.todo_list.demo.todo.service;
import codej.todo_list.demo.todo.dto.ProductDto;
import codej.todo_list.demo.todo.dto.ProductResponseDto;
public interface ProductService {
ProductResponseDto getProduct(Long number);
ProductResponseDto setProduct(ProductDto dto);
ProductResponseDto changeProductName(Long number,String name);
void deleteProduct(Long number);
}
package codej.todo_list.demo.todo.service;
import codej.todo_list.demo.todo.dto.ProductDto;
import codej.todo_list.demo.todo.dto.ProductResponseDto;
import codej.todo_list.demo.todo.entity.ProductEntity;
import codej.todo_list.demo.todo.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService{
private final ProductRepository productRepository;
@Override
public ProductResponseDto getProduct(Long number) {
ProductEntity product = productRepository.findById(number).get();
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setPno(product.getPno());
productResponseDto.setName(product.getName());
productResponseDto.setPrice(product.getPrice());
productResponseDto.setStock(product.getStock());
return productResponseDto;
}
@Override
public ProductResponseDto setProduct(ProductDto dto) {
ProductEntity product = new ProductEntity();
product.setName(dto.getName());
product.setPrice(dto.getPrice());
product.setStock(dto.getStock());
ProductEntity savedProduct = productRepository.save(product);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setPno(savedProduct.getPno());
productResponseDto.setName(savedProduct.getName());
productResponseDto.setPrice(savedProduct.getPrice());
productResponseDto.setStock(savedProduct.getStock());
return productResponseDto;
}
@Override
public ProductResponseDto changeProductName(Long number, String name) {
ProductEntity foundProduct = productRepository.findById(number).get();
foundProduct.setName(name);
ProductEntity changedProduct = productRepository.save(foundProduct);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setName(changedProduct.getName());
productResponseDto.setPno(changedProduct.getPno());
productResponseDto.setPrice(changedProduct.getPrice());
productResponseDto.setStock(changedProduct.getStock());
return productResponseDto;
}
@Override
public void deleteProduct(Long number) {
productRepository.deleteById(number);
}
}
package codej.todo_list.demo.todo.controller;
import codej.todo_list.demo.todo.dto.ProductResponseDto;
import codej.todo_list.demo.todo.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/product")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping()
public ResponseEntity<ProductResponseDto> getProduct(Long number) {
ProductResponseDto result = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
}
package codej.todo_list.demo.todo.repository;
import codej.todo_list.demo.todo.entity.ProductEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<ProductEntity,Long> {
}
JUnit은 자바 언어에서 사용되는 대표적인 테스트 프레임워크로서 단위 테스트를 위한 도구를 제공합니다.
또한, 단위 테스트 뿐만 아니라 통합 테스트를 할 수 있는 기능도 제공합니다.
- JUnit의 가장 큰 특징은 어노테이션 기반의 테스트 방식을 지원합니다.
- 즉, JUnit을 사용하면 몇개으 ㅣ어노테이션만으로도 간편하게 테스트 코드를 작성할 수 있습니다.
- 또한 Junit을 활용하면 단정문(Assert)를 통해 테스트 케이스의 기대값이 정상적으로 도출되었는지 검토 할 수 있다는 장점이 있습니다.
이처럼 JUnit은 하나의 Platform 모듈을 기반으로 Jupiter,Vintage 모듈이 구현체의 역할을 수행합니다.
생명주기와 관련되어 테스트 순서에 관여하게 되는 대표적인 어노테이션은 다음과 같습니다.
@Test : 테스트 코드를 포함한 메서드를 정의합니다.
@BeforAll : 테스트를 시작하기전에 호출되는 메서드를 정의합니다.
@BeforEach : 각 테스트 매서드가 실행되기 전에 동작하는 메서드를 정의합니다.
@AfterAll : 테스트를 종료하면서 호출되는 메서드를 정의합니다.
@AfterEach : 각 테스트 메서드가 종료되면서 호출되는 메서드를 정의합니다.
@BeforeAll 과 @AfterAll 어노테이션이 지정된 메서드는 전체 테스트 동작에서 처음과 마지막에만 각각 수행됩니다.
@BeforeEach 와 @AfterEach는 @Test 어노테이션이 지정된 테스트 메서드를 기준으로 실행됩니다.
@RestController
@RequestMapping("/product")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping()
public ResponseEntity<ProductResponseDto> getProduct(Long number) {
ProductResponseDto result = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
}
import codej.todo_list.demo.todo.controller.ProductController;
import codej.todo_list.demo.todo.dto.ProductResponseDto;
import codej.todo_list.demo.todo.service.ProductServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(ProductController.class)
// 웹에서 사용되는 요청과 응답에 대한 테스트를 수행할 수 있습니다.
// 대상 클래스만 로드해 테스트를 수해앟며, 만약 대상을 클래스에 추가하지 않으면 @Controller,@RestController,@ControllerAdvice
// 등의 컨트롤러 관련 빈객체가 모두 로드 됩니다. @SpringBootTest 보다 가렵게 테스트하기 위해 사용됩니다.
public class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
// @MockBean은 실제 빈 객체가 아닌 Mock(가짜) 객체를 생성해서 주입하는 역할을 수행합니다.
// @MockBean이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않습니다.
// 그렇기 때문에 해당 객체는 개발자가 Mockito의 given() 메서드를 통해 동작을 정의해야 합니다.
ProductServiceImpl productService;
@Test
// 테스트 코드가 포함되어 있다고 선언하는 어노테이션이며, JUnit Jupiter에서는 이 어노테이션을 감지해서 테스트 계획에 포함시킵니다.
@DisplayName("MockMVC를 통한 Product 데이터 가져오기 테스트")
// 테스트 메서드의 이름이 복잡해서 가독성이 떨어질 경우 이 어노테이션을 통해 테스트에 대한 표현을 정의할 수 있습니다.
void getProductTest() throws Exception {
given(productService.getProduct(123L)).willReturn(
new ProductResponseDto(123L,"pen",5000,2000));
String productId = "123";
mockMvc.perform(
get("/product?number="+ productId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.pno").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).getProduct(123L);
}
}
일반적으로 @WebMvcTest 어노테이션을 사용한 테스트는 슬라이스(Slice) 테스트라고 부릅니다.
슬라이스 테스트는 단위 테스트와 통합 테스트의 중간 개념으로 이해하면 되는데, 레이어드 아키텍처를 기준으로 각 레이어별로 나누어 테스트를 진행한다는 의미입니다.
단위 테스트를 수행하기 위해서는 모든 외부 요인을 차단하고 테스트를 진행해야 하지만 컨트롤러는 개념상 웹과 맞닿은 레이어로서 외부 요인을 차단하고 테스트하면 의미가 없기 때문에 슬라이스 테스트를 진행하는 경우가 많습니다.
@MockBean 어노테이션을 통해 ProductController가 의존성을 가지고 있던 ProductService 객체에 Mock 객체를 주입하였습니다.
Mock 객체에는 테스트 과정에서 맡을 동작을 정의해야합니다.
given() 메서드를 통해 이 객체에서 어떤 메서드가 호출되고 어떤 파라미터를 주입받는지 가정한 후 willReturn() 메서드를 통해 어떤 결과를 리턴할 것인지 정의하는 구조로 코드를 작성합니다.
메서드 이름에서 알 수 있듯이 이 부분의 코드가 앞에서 설명한 Given에 해당합니다.
MockMvc는 컨트롤러의 API를 테스트하기 위해 사용된 객체입니다.
정확하게는 서블릿 컨테이너의 구동 없이 가상의 MVC 환경에서 모의 HTTP 서블릿을 요청하는 유틸리티 클래스입니다.
perform() 메서드를 통해서 서버로 URL을 요청을 보내는 것처럼 통신 테스트 코드를 작성해서 컨트롤러를 테스트 할 수 있습니다.
그리고 perform() 메서드의 결과 값으로 ResultActions 객체가 리턴되는데, andExpect() 메서드를 사용해 결과값 검증을 수행할 수 있습니다.
요청과 응답의 전체 내용을 확인하려면 andDo() 메서드를 사용합니다.
마지막으로 verify() 메서드는 지정된 메서드가 실행되었는지 검증하는 역할입니다.
@Test
@DisplayName("Product 데이터 생성 테스트")
void createProductTest() throws Exception {
// Mock 객체에서 특정 메서드가 실행되는 경우 실제 Return을 줄 수 없기 때문에 아래와 같이 가정 사항을 만들어줍니다.
given(productService.setProduct(new ProductDto("pen",5000,200)))
.willReturn(new ProductResponseDto(12345L,"pen",5000,200));
ProductDto productDto = ProductDto.builder()
.name("pen")
.price(5000)
.stock(200)
.build();
Gson gson = new Gson();
String content = gson.toJson(productDto);
mockMvc.perform(
post("/product")
.content(content)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.pno").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).setProduct(new ProductDto("pen",5000,200));
}