kok202
스프링 부트 강의 정리 (19~20 : 테스트, MockBean)

2019. 4. 28. 00:47[공부] 영상/스프링 부트 강의

스프링 부트 테스트

spring-boot-starter-test 를 사용하고 이 안에 spring-boot-test, spring-boot-test-autoconfigure 가 있다. JUnit, AssertJ, Harmcrest 라이브러리들을 가져온다.

 

AssertJ : matcher 도 안쓰고 assertThat 만으로 Chaining 해서 테스트 코드 검증할 수 있어서 괜찮은 듯

https://joel-costigliola.github.io/assertj/

 

AssertJ / Fluent assertions for java

AssertJ Fluent assertions for java

joel-costigliola.github.io

 

Dependency Injection의 가장 큰 장점은 테스트 코드 작성이 쉬워진다는 것이다. 게다가 인젝션이 필요없는 경우에 따라서 new 해서 객체를 생성할 수도 있다. Spring boot 테스트는 Application Context 쓰듯이 쓰면 된다.@SpringBootTest는 스프링 테스트를 위한 @ContextConfigurtaion 파일을 불러 올 수 있다.

 

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<scope>test</scope> 을 추가해주면 src/test에서만 의존성이 걸리고 test 영역안에서만 사용가능하다는 의미다.

 

 

 

 

 

@SpringBootTest

이 어노테이션이 붙어있으면 Test 용 ApplicationContext를 만들어준다. ApplicationContext 에 관련된 설정은 @SpringBootApplication 달려있는 메인 어플리케이션을 기준으로 메인 어플리케이션에 붙은 Configuration 관련 설정들을 불러온다. 테스트 클래스가 메인 어플리케이션을 찾는 알고리즘은 다음과 같다.

1. 테스트 클래스가 속해있는 패키지를 불러온다.

2. 불러온 패키지 경로와 같은 패키지 경로를 src/main에서 찾는다.

3. 찾아낸 패키지 경로의 하위 클래스들을 검색한다.

 

@SpringBootApplication 에 해당하는 모든 설정파일을 읽어들이면 테스트 하나 돌리는데 너무 많은 Bean 파일을 불러와야 할 수도 있다. 그래서 컨테이너를 띄우는데만 20초, 30초 걸려서 테스트 속도가 매우 느릴 수 있다. 이 때  @Configuration 인 클래스를 테스트 클래스의 Inner 클래스로 등록하면 테스트할 때 사용하는 설정 파일을 @Configuration Inner 클래스로 대체시킬 수 있다. 그렇게하여 원하는 빈파일만 불러오도록 할 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyControllerTest{
    @Configuration
    @ComponentScan(basePackageClasses={MyController1.class, MyController2.class})
    static class TestConfig{
    }
    
    @Autowired
    String myBean;
    
    @Test
    public void test(){
        assertThat(myBean).isNotNull();
    }
}

설정 파일을 Inner 클래스로 만들 때는 static 을 줘야한다.

 

@TestConfiguration

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyControllerTest{
    @TestConfiguration
    static class TestConfig{
        @Bean
        public String myBean(){
            return "hello world";
        }
    }
    
    @Autowired
    String myBean;
    
    @Test
    public void test(){
        assertThat(myBean).isNotNull();
    }
}

Inner 클래스의 @Configuration 이 아예 덮어쓰기 해버리는 거라면 @TestConfiguration은 추가적인 빈파일을 불러오는 용도로 사용할 수 있다.

 

@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.MOCK) 일 경우 (기본 값)

MOCK 의 사전적 의미 : 모조품

서블릿 API가 클래스 안에 있으면 WebApplicationContext를 불러오고, 내장 서블릿 컨테이너가 실행하지 않고 MOCK servlet 환경을 만든다. 서블릿 API가 클래스안에 없으면 ApplicationContext를 만든다. 가짜 서블릿이라서 테스트 로그를 자세히보면 TestDispatcherServlet이 있는 것을 확인 할 수 있다.

 

@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT) 일 경우

진짜 서블릿 환경을 제공하고 진짜 서블릿 컨테이너를 random port로 열어서 테스트에 사용한다. @LocalServerPort int port; 를 이용하면 Random port 로 생성된 포트를 가져올 수 있다.

 

@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.DEFINED_PORT) 일 경우

진짜 서블릿 환경을 제공하고 진짜 서블릿 컨테이너를 고정 port로 열어서 테스트에 사용한다.

 

@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.NONE) 일 경우

진짜 서블릿 환경을 제공하고 진짜 서블릿 컨테이너를 random port로 열어서 테스트에 사용한다.

 

@Import(MyClass.class)

명시적으로 원하는 클래스를 불러올 수 있다.

 

 

 

 

컨트롤러를 테스트해보기 위한 2개의 방법이 있다.

방법 1. Controller를 직접 Autowired 받거나 new 해서 메소드를 호출해본다.

방법 2. 클라이언트를 생성하고 클라이언트로 서버와 통신해본다. (권장)

 

클라이언트를 생성하는데는 3가지 방법이 존재한다.

1. TestRestTemplate : 동기로 동작한다.

2. TestWebClient : Reactive, Reactive이기 때문에 webFlux가 dependency로 추가되어야한다.

3. MockMvc (권장)

 

@AutoConfigureMockMvc

Mock Mvc관련 설정파일을 불러온다.

@Autowired MockMvc mockMvc; 를 사용할 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyControllerTest{
    @Autowired
    MockMvc mockMvc;
    
    @Test
    public void test(){
        mockMvc.perform(get("/test")
            .andExpect(status().isOk())
            .andDo(print());
    }
}

 

 

 

 

 

Mocking 과 Spying

테스트할 때 원격 서버의 api 와 통신해봐야 할 수도 있다. 그런데 이렇게 되면 테스트 코드가 원격 서버에 의존적이다. 원격 서버와 통신해야하므로 테스트 코드의 성능도 문제가 된다. 또한 원격 서버가 다운되어 있을 때를 테스트하는 코드가 작성되야 할 수도 있다. 이런 경우 실제로 원격 서버를 다운시킬 수도 없는 노릇이다.이럴 때를 위해서 Spring boot 는 @MockBean과 @SpyBean을 제공한다.

 

 

 

 

 

@MockBean

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTests {
	@Autowired
	private MyService myService;
    
	@MockBean
	private MyClient myClient;

	@Test
	public void exampleTest() {
		given(myClient.call()).willReturn("hello");
		String result = myService.whatRecieved();
		assertThat(result).isEqualTo("I recieve hello");
	}
}

가정

1. MyClient 는 원격 서버와 통신하는 클라이언트다.

2. MyService 는 MyClient 를 주입받아서 사용한다.

3. MyService 의 whatRecieved() 메소드는 원격 서버에서 받은 값을 "I recieve ~ " 으로 포맷팅해서 return 해준다.

 

@MockBean 의 동작 : MyService 가 MyClient 를 주입받을 때 @MockBean으로 만든 myClient 가 주입된다. 그리고 myClient 의 call() 이라는 메소드는 "hello"을 return 하도록 Mocking 해놨다. MyService 가 whatRecieved 를 호출하면 myClient 의 call 메소드가 연쇄 호출 될 것이고 myClient의 call 메소드가 호출 될 때 실제로 call 메소드를 호출하지 않고 mocking 된 결과 값인 hello 가 return 된다.

 

 

 

 

 

@SpyBean
MockBean 으로 등록된 객체는 빈 객체이기 때문에 만약 MockBean 으로 등록된 객체가 아무것도 mocking 하지 않으면 MockBean 객체의 메소드의 실행은 의미 없는 값을 반환하게 된다. ex) 만약 반환 타입이 String 일 경우 "No content" 가 반환된다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTests {
	@Autowired
	private MyService myService;
    
	@MockBean
	private MyClient myClient;

	@Test
	public void exampleTest() {
		String result = myService.whatRecieved();
		assertThat(result).isEqualTo("I recieve hello");
	}
}

위 코드의 myService.whatRecieved(); 결과는 "I recieve No content" 이므로 테스트는 실패한다.

@SpyBean의 동작은 Mocking 하지 않은 메소드에 대해서 기존의 제대로된 메소드를 실행시킨다.

@SpyBean은 기존의 객체에 있는 코드를 그대로 사용하면서 일부만 변경한다. 

 

 

 

 

 

 

테스트시 유의사항

  • 만약 테스크 클래스가 @Transactional 이 달려있으면 모든 테스트가 transaction으로 동작한다.
  • @RunWith(SpringRunner.class) 이 어노테이션이 없으면 스프링 관련 어노테이션이 전부 동작하지 않으니 꼭 추가하는 것을 잊지 말아야한다.