Skip to content

Latest commit

 

History

History
431 lines (306 loc) · 13.7 KB

2020-04-12-test.md

File metadata and controls

431 lines (306 loc) · 13.7 KB

Spring Boot Test

스프링 부트에서는 기본적인 테스트 스타터를 제공한다. 스타터에 웬만한 테스트 라이브러리들을 뭉쳐놓아 편리하게 사용할 수 있다.

  • spring-boot-test
  • spring-boot-test-autoconfigure

위 두개 모듈이 테스트 관련 자동 설정 기능을 제공하고, 일반적으로 spring-boot-starter-test로 두 모듈을 함께 사용한다.

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

단위 테스트(JUnit)

Spring Boot에서 JUnit5를 이용해 테스트 코드를 작성해보기 전에 기본적인 내용에 대해서 다루고 넘어갈 것이다. 일반적으로 단위 테스트(Unit Test) 코드를 작성할 때 5가지 원칙을 강조한다.

  • Fast : 테스트 코드를 실행하는 일은 오래 걸리면 안된다.
  • Independent : 독립적으로 실행이 되어야한다.
  • Repeatable : 반복 가능해야한다.
  • Self Validation : 메뉴얼 없이 테스트 코드만 실행해도 성공, 실패 여부를 알 수 있어야한다.
  • Timely : 바로 사용 가능해야한다.

Junit

Junit은 Java의 단위 테스팅 도구이다.

  • 단위 테스트 Framework중 하나
  • 단정문으로 Test Case 수행결과를 판별한다.
  • Annotation으로 간결하게 사용가능하다.

Dependencies

2.2.0 이후 버전에서는 Junit5가 기본으로 변경되었다.

SpringBoot 2.2.0 이전 버전에서 junit5 설정

  • maven

    <!-- spring boot test junit5 사용 exclusion을 통해 junit4에서 코드 실행시 사용하는 vintage-engine 예외처리-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    
    <!-- 테스트 코드 작성에 필요한 junit-jupiter-api 모듈과 테스트 실행을 위한 junit-jupiter-engine 모듈 포함 -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
    </dependency>
  • gradle

    testImplementation ('org.springframework.boot:spring-boot-starter-test') {
    	exclude module: 'junit'
    }
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testRuntimeOnly'org.junit.jupiter:junit-jupiter-engine'
    

Test 단위

설명 Bean
@SpringBootTest 통합 테스트, 전체 Bean 전체
@WebMvcTest 단위 테스트, Mvc 테스트 MVC 관련된 Bean
@DataJpaTest 단위 테스트, Jpa 테스트 JPA 관련 Bean
@RestClientTest 단위 테스트, Rest API 테스트 일부 Bean
@JsonTest 단위 테스트, Json 테스트 일부 Bean
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

/**
 * ExtendWith : JUnit5 확장 기능
 *
 */
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = T2020AprilApplicationTests.class)
@ActiveProfiles("test")
@Transactional
public class T2020AprilApplicationTests {

	@Test
	void contextLoads() {
	}

}
  • @SpringBootTest 는 통합 테스트를 제공하는 기본적인 Spring Boot Test 어노테이션이다. 여러 단위 테스트를 하나의 통합된 테스트로 수행할 때 적합하다.

    • @SpringBootApplication 이 붙은 어노테이션을 찾아 context를 찾는다.

    • 실제 구동되는 애플리케이션과 똑같이 애플리케이션 컨텍스트를 로드해 테스트하기 때문에 하고 싶은 테스트를 모두 수행할 수 있다.

    • 애플리케이션에서 설정된 빈을 모두 로드하기 때문에 애플리케이션 규모가 클수록 느려진다.

    • properties : 테스트 실행되기전 {key,value} 형식으로 프로퍼티를 추가할 수 있다.

      @SpringBootTest(properties = {"property.value= propertyTest"})
    • value : 테스트 실행 전 적용할 프로퍼티를 주입할 수 있다.

      @SpringBootTest(value= "value=test")
    • classes : 애플리케이션 컨텍스트에 로드할 클래스를 지정할 수 있다. 별도로 설정하지 않으면, @SpringBootApplication or @SpringBootConfiguration 을 찾아서 로드한다.

      @SpringBootTest(classes= {SpringBootTestApplication.class})
  • @ExtendWith 는 확장기능을 구현한다.

    • JUnit5에서 제공하는 기능의 상당수가 이 기능으로 지원되고 있다.
    • 실제 기능이 해당 어노테이션을 통해서 실행된다.
    • JUnit4에서 RunWith와 유사하다.
  • @ActiveProfiles원하는 프로파일 환경 값을 부여할 수 있다.

  • @Transactional : 테스트를 마치고 나서 수정된 데이터가 롤백된다.

MockMvc Test

MockMvc 는 브라우저에서 요청과 응답을 의미하는 객체로서 웹에서 테스트하기 힘든 Controller 테스트를 용이하게 해준다. 또한 시큐리티 혹은 필터까지 자동으로 테스트해 수동으로 추가/삭제도 가능하다.

@WebMvcTest로 테스트를 할 수 있다.

  • Book.java

    package com.example.boot.domain;
    
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    import java.time.LocalDateTime;
    
    @NoArgsConstructor
    @Getter
    public class Book {
        private Integer idx;
        private String title;
        private LocalDateTime publishedAt;
    
        @Builder
        public Book(String title, LocalDateTime publishedAt){
            this.title = title;
            this.publishedAt = publishedAt;
        }
    }
  • Service

    package com.example.boot.service;
    
    import com.example.boot.domain.Book;
    
    import java.util.List;
    
    public interface BookService {
        List<Book> getBookList();
    }
  • Controller

     package com.example.boot.controller;
     
    import com.example.boot.service.BookService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class BookController {
    
       @Autowired
       private BookService bookService;
    
       @GetMapping("/books")
       public String getBookList(Model model) {
           model.addAttribute("bookList", bookService.getBookList());
           return "book";
       }
    }
  • test

    package com.example.boot;
    
    import com.example.boot.controller.BookController;
    import com.example.boot.domain.Book;
    import com.example.boot.service.BookService;
    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.hamcrest.Matchers.contains;
    import static org.mockito.BDDMockito.given;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    
    import java.time.LocalDateTime;
    import java.util.Collections;
    
    @WebMvcTest(BookController.class)
    public class BookControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @MockBean
        private BookService bookService;
    
        @Test
        public void Book_MVC_TEST() throws Exception {
            Book book = new Book("Spring Boot Book", LocalDateTime.now());
            given(bookService.getBookList()).willReturn(Collections.singletonList(book));
            mockMvc.perform(get("/books"))
                    .andExpect(status().isOk())
                    .andExpect(view().name("book"))
                    .andExpect(model().attributeExists("bookList"))
                    .andExpect(model().attribute("bookList",contains(book)));
        }
    
    }
  • @WebMvcTest(BookController.class) : 테스트할 컨트롤러명을 명시해준다.

  • 여기서 @Service@WebMvcTest의 적용 대상이 아니다. BookService 인터페이슬르 구현한 구현체는 없지만, @MockBean을 적극적으로 활용해 컨트롤러 내부의 의존성 요소를 각자 객체로 대체 했다. MockBean은 실제 객체는 아니지만 실제 객체처럼 동작하게 만들 수 있다.

    • given(bookService.getBookList()).willReturn(Collections.singletonList(book)); : 가짜 객체를 만들어 given()을 사용해 메서드 실행에 대한 반환값을 미리 설정했다.
  • 그후 andExpect()로 예상 값이 나오는지에 대해서 테스트를 진행했다.

만약 기본설정만 필요하다면 @AutoConfigureMockMvc 으로 간단하게 설정할 수 있다.

@SpringBootTest
@AutoConfigureMockMvc
public abstract class AbstractControllerTest {
    
	@Autowired
	protected MockMvc mockMvc;

}

하지만 AutoConfig로 설정을 하면 Custom하기 어려워진다. 아래 코드는 MockMvcBuilders를 활용해 MockMvc 인스턴스를 생성해주었다. 이렇게 공통 추상클래스를 만들어 사용할 수 있다.

/**
 * 테스트시 필요한 커스텀 공통 설정 추상 클래스
 */
@SpringBootTest
public abstract class AbstractControllerTest {
	
	protected MockMvc mockMvc;
	
	abstract protected Object controller();

	@BeforeEach
	private void setup() {
		mockMvc = MockMvcBuilders.standaloneSetup(controller()) 						 // 기본설정 
			.addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true)) // 테스트 수행시 한글 깨짐 방지
			.alwaysDo(print())															 // 항상 콘솔 출력
			.build();
		
	}
}

이제 추상 클래스를 상속받아 Controller 테스트를 수행할 수 있다.

/**
 * 테스트시 필요한 공통 설정 추상 클래스(AbstractController) 상속
 * PaymentGatewayController 테스트 클래스
 * TestMethodOrder : OrderAnnotaion기준으로 테스트 메소드 수행  
 */
@TestMethodOrder(OrderAnnotation.class)
public class PaymentGatewayControllerTest extends AbstractControllerTest {

	private static String[] pmtCodeArr = {"P0001", "P0001", "P0002", "P0003", "P0003", "P0004", "P0005"}; // pmtCode 테스트 데이터 배열
	private static String[] mbrIdArr = {"0000000345", "0000000911", "0000000602"}; 	// mbrId 테스트 데이터 배열

	
	@Autowired
	PaymentGatewayController paymentGatewayController;
	
	
	/**
	 * @return 테스트할 paymentController 인스턴스
	 */
	@Override
	protected Object controller() {
		// TODO Auto-generated method stub
		return paymentGatewayController;
	}
 	
	
	/**
	 * Test method for {@link com.example.test.controller.PaymentGatewayController#approve(java.lang.String, java.lang.String, java.lang.String, long)}.
	 * 결제 승인 요청 테스트
	 * 각각 mbrId별로 pmtCodeArr 데이터로 생성
	 * 이떄, pmtType은 null로 보낸다.(자동으로 생성되도록) 
	 * Order annotation은 테스트 실행순서 지정
	 */
	@Test
	@Order(1)
	void testApprove() {
	     try {
	    	 
	    	 for(int i=0;i<7;i++) {
	    		 for(String mbrId : mbrIdArr) {
	    			 MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
	    				
	    			 params.add("mbrId", mbrId);
	    			 params.add("pmtCode", pmtCodeArr[i]);
	    			 params.add("pmtType", "");
	    			 params.add("pmtAmt", "157400");
	    			 
             // curl -X POST "http://localhost:8080/api/pg/approve?mbrId=&pmtAmt=&pmtCode=" -H "accept: */*"
	    			 mockMvc.perform(post("/api/pg/approve")
	    					 			.contentType(MediaType.APPLICATION_JSON)
	    					 			.params(params))
	    			 					.andExpect(status().isOk()); // 성공여부 확인
	    		 }
	    	 }
	    	
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Test method for {@link com.example.test.controller.PaymentGatewayController#getRecentPaymentList(java.lang.String, java.lang.String, java.lang.Integer)}.
	 * 최근 결제내역리스트 조회 테스트
	 * 각각 member 별로 10개씩 조회
	 */
	@Test
	@Order(2)
	void testGetRecentPaymentList() {
		
		try {
			for(String mbrId : mbrIdArr) {
				MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
				params.add("mbrId", mbrId);
				params.add("size", "10");
				
        // curl -X GET "http://localhost:8080/api/pg/approve?mbrId=&pmtAmt=&pmtCode=" -H "accept: */*"
				mockMvc.perform(get("/api/pg/getRecentPaymentList")
						.params(params))
						.andExpect(status().isOk()) 							// 수행결과 확인
				 		.andExpect(jsonPath("$[*]", hasSize(10))) 					// 각 멤버별 리스트 수 확인 
				 		.andExpect(jsonPath("$[?(@.succYn =='Y')]", hasSize(9)))	// 성공여부 성공(Y) 9개 확인
				 		.andExpect(jsonPath("$[?(@.succYn =='N')]", hasSize(1)));	// 성공여부 실패(N) 1개 확인 
				
			}
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 		
	}

	

}
  • @Test : 테스트 대상을 지정
  • @Order : 테스트 수행 순서 지정
  • @TestMethodOrder(OrderAnnotation.class) : OrderAnnotation 기준으로 테스트 수행

참조