DongDD's IT

[Spring] Spring Test 본문

프로그래밍/Spring

[Spring] Spring Test

DongDD 2019. 4. 20. 18:48

[Spring] Spring Test



Spring Test


- Spring Framework에서 동작하는 클래스를 테스트하는 모듈


Unit Test


- 테스트할 클래스의 구현 내용만 테스트

- 사용하는 다른 클래스의 method 등은 Mock 또는 Stub으로 만들어 해당 구현 내용에 영향을 끼치지 않게함


Integration Test


- 실제 운영 환경에서 사용될 클래스를 통합하여 테스트

- 기능 검증을 위한 것이 아닌 spring framework에서 제대로 동작하는지 검증



Bean Test


Dependency 추가


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.23.4</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
cs

Mockito


- 해당 클래스에 대해서만 테스트하기 위해 다른 클래스를 참조하는 값을 임의로 설정(Mocking, 가짜 객체로 만들어줌)

- 참조 : https://dongdd.tistory.com/165?category=767942


Unit Test


1. Service

1) JunitService1

1
2
3
4
5
6
7
8
@Service
public class JunitService1 {
    @Autowired
    private JunitService2 junitService2;
    public String get() {
        return junitService2.get();
    }
}
cs


2) JunitService2

1
2
3
4
5
6
@Service
public class JunitService2 {
    public String get() {
        return "test";
    }
}
cs

- Service1에서 Service2의 get 함수를 호출하고 Service2의 get 함수는 "test"를 return


2. Spring MVC Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
@WebAppConfiguration
public class TestJUnitService {
    @InjectMocks
    private JunitService1 junitService1;
 
    @Mock
    private JunitService2 junitService2;
    @Test
    public void serviceTest() {
        BDDMockito.given(junitService2.get())
                .willReturn("test1");
        String returnValue = junitService1.get();
        assertThat(returnValue, is("test1"));
    }
}
cs

- @Runwith : 기본으로 지정된 Test Runner 대신 지정하여 사용 → Test Runner 확장

-> TestRunner : Junit Framework에서 테스트 클래스 내의 method의 실행을 담당

- @ContextConfiguration : Application Context를 만들기 위한 설정 파일 지정

- @WebAppConfiguration : 가상의 web.xml(web app 전용 Application Context/DI)을 생성

-> Servlet API를 사용한 Mock 객체 생성 가능

- @InjectMocks : Test할 클래스를 지정하여 inject

- @Mock : Test할 클래스에서 사용하는 클래스을 mocking

- @Test : method에 지정하여 해당 method가 test를 진행하게 함

- BDDMockito를 이용하여 Service1이 Service2의 get()을 호출할 경우, "test" 를 return하도록 mocking

- assertThat을 사용하여 Service1이 제대로 동작했을 때의 return 값이 "test"와 일치하는지 확인

- 결과(성공)

1

- 결과(실패)

2


3. Spring Boot Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(SpringRunner.class)
@Import({JunitService1.class})
public class TestJunitService1 {
    @Autowired
    private JunitService1 junitService1;
 
    @MockBean
    private JunitService2 junitService2;
    @Test
    public void testJunit() {
        BDDMockito.given(junitService2.get())
                .willReturn("test1");
 
        String result = junitService1.get();
        assertThat(result,is("test1"));
    }
}
cs

- Test Class에 @RunWith(SpringRunner.class)를 지정

- @Import({JunitService1.class}) : Test할 class를 import

- @InjectMocks 대신 @Autowired로 가져옴

- 해당 클래스에서 사용하는 다른 클래스들을 @MockBean으로 지정 

- 나머지는 동일

- mvc와 같이 @InjectMocks, @Mock 사용해도 가능(Import 없앤 후 실행)

- 결과(성공)

3

- 결과(실패)

4


Integration Test


- JUnit에서 DI Container를 가동 후, 실제 환경처럼 테스트


1. Spring MVC Test

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
@WebAppConfiguration
public class TestJUnitServiceIntegration {
    @Autowired
    private JunitService1 junitService1;
 
    @Test
    public void test() {
        String result = junitService1.get();
        assertThat(result,is("test"));
    }
}
cs

- 실제로 juniService1.get()을 실행했을 때, return되는 값을 test

- mock으로 원하는 테스트 케이스를 설정하는 것이 아닌 실제 동작하여 나오는 결과를 test

- 결과(성공)

5

- 결과(실패)

6


2. Spring Boot

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestJUnitServiceIntegration {
    @Autowired
    private JunitService1 junitService1;
 
    @Test
    public void test() {
        String result = junitService1.get();
        assertThat(result, is("test1"));
    }
}
cs
- 실제로 juniService1.get()을 실행했을 때, return되는 값을 test

- @SpringBootTest : test에 사용할 Application Context(DI Container) 조작

- mock으로 원하는 테스트 케이스를 설정하는 것이 아닌 실제 동작하여 나오는 결과를 test

- 결과(성공)

7

- 결과(실패)

8


DI Container


1. Cache

- Test DI Container는 테스트 종료 전까지 캐시됨

-> 테스트 케이스간 공유하여 사용 가능

- @ContextConfiguration에 지정된 속성 값이 같으면 캐시 DI Container 사용

-> Test 시간 단축


2. DI Container 제거

- Test DI Container는 Java VM 시작 시 생성되고 종료 시 사라짐

- @DirtiesContext : 해당 annotation 지정 시, 지정한 클래스/메소드가 종료 시 Di Container 제거


Profile 지정


- application.yml 파일에 지정했던 profile 별 Data source들을 사용하기 위해 Test에 지정할 수 있음

- 참조 : https://dongdd.tistory.com/163

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringRunner.class)
@Import({JunitService1.class})
@ActiveProfiles("test")
public class TestJunitService1 {
    @Autowired
    private JunitService1 junitService1;
 
    @MockBean
    private JunitService2 junitService2;
 
    @Test
    public void testJunit() {
        BDDMockito.given(junitService2.get())
                .willReturn("test");
 
        String result = junitService1.get();
 
        assertThat(result,is("test1"));
    }
}
cs

- @ActiveProfiles : 설정한 profile 중 어떤 profile을 선택하여 data source를 사용할지 설정


Property 지정


- @Value를 이용하여 property의 value를 이용하는 값을 test code에 사용할 때 원하는 값으로 변경하여 사용할 수 있음


1. application.properties

1
2
spring.application.name = test
server.port = 8080
cs


2. property 값 변경하여 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
@WebAppConfiguration
@TestPropertySource(properties = "spring.application.name=aaa")
public class TestJUnitService {
    @Value("${spring.application.name}")
    private String appName;
 
    @Test
    public void nameTest() {
       assertThat(appName,is("test"));
    }
}
cs

- @TestPropertySource(properties = "spring.application.name=aaa")와 같이 사용하여 property 값을 실제와 다르게 변경하여 사용할 수 있음

- 결과

9


3. property 파일 지정

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
@WebAppConfiguration
@TestPropertySource(locations = "/application.properties")
public class TestJUnitService {
    @Value("${spring.application.name}")
    private String appName;
 
    @Test
    public void nameTest() {
       assertThat(appName,is("test"));
    }
}
cs

- application.properties 파일의 위치 지정

- 결과

10



DataBase Test


- Database에 접근하는 bean을 테스트


Database Test Example


1. Test Datasource 생성

1
2
3
4
5
6
7
8
9
@Configuration
public class TestAppConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .setScriptEncoding("UTF-8").build();
    }
}
cs

- Test하기 위해 필요한 datasource를 config파일에 정의

- 내장형 DB H2 사용


2. Repository 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class TRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;
 
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
 
    public void create(TestModel testModel) {
        jdbcTemplate.update("INSERT INTO test(id, name) VALUES(?, ?)",
 testModel.getId(), testModel.getName());
    }
 
    public String get(Long id) {
        return jdbcTemplate.queryForObject("SELECT name FROM test WHERE id = ?"
String.class, id);
    }
}
cs

- insert를 위한 create() method 정의

- select를 위한 get() method 정의


3. Table 생성 및 Data insert/select

1) sql 파일

1
2
3
4
create table TEST (
  id int primary key,
  name varchar(30)
);
cs

- sql파일에 create table query를 작성

- create table을 위한 sql 파일 생성(test.sql)


2) Test Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
@WebAppConfiguration
public class TestTRepository {
    @Autowired
    private TRepository tRepository;
 
    @Test
    @Sql(value = "/test.sql")
    @Order(1)
    public void insert() {
        TestModel testModel = new TestModel();
        testModel.setId(Long.valueOf(123));
        testModel.setName("test");
        tRepository.create(testModel);
    }
 
    @Test
    @Order(2)
    public void select() {
        assertThat(tRepository.get(Long.valueOf(123)), is("test"));
    }
}
cs

- @Sql을 사용하여 해당 sql 파일을 실행

- insert하기 전 한번 수행되고 (123,"test") 값을 repository에 insert

- 이 후에 select하는 test가 수행되고 값이 제대로 들어간 것을 확인

- 순서를 보장하기 위해 @Order를 사용하여 Insert가 먼저 수행되게 함

- 결과(성공)

11

- 결과(실패)

12


Transaction


- 같은 DB를 여러 환경에서 Test하는 경우, 테스트를 수행하는 트랜잭션을 분리할 필요가 있음

- @Transactional 이용 -> default : Test method 실행 후 rollback

- commit이 필요한 method의 경우 @Commit 이용


1. Config 내용 추가

1
2
3
4
5
6
7
@Configuration
public class TestAppConfig {
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}
cs

- @Transactional을 사용하기 위해 TransactionManager 설정 필요


2. Insert/Select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
@WebAppConfiguration
@Transactional
public class TestTRepository {
    @Autowired
    private TRepository tRepository;
 
    @Test
    @Sql(value = "/test.sql")
    @Commit
    @Order(1)
    public void insert() {
        TestModel testModel = new TestModel();
        testModel.setId(Long.valueOf(123));
        testModel.setName("test");
        tRepository.create(testModel);
    }
 
    @Test
    @Order(2)
    public void select() {
        assertThat(tRepository.get(Long.valueOf(123)), is("test"));
    }
}
cs

- Transacional로 지정하고 insert된 값을 확인하기 위해 insert method에 @Commit 지정

- default : rollback

- 결과(성공)

13

- 결과(실패, Commit 없을 시)

14


Persistence Context


- JPA나 Hibernate의 경우, 인 메모리에 쌓아두었다가 commit될 때 sql을 실행하는 방식을 사용

- test code의 기본 룰은 rollback이기 때문에 테스트에 어려움이 생길 수 있음

- flush를 사용하여 명시적으로 sql문을 실행


1. JPA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
@WebAppConfiguration
public class TestTRepository {
    @Autowired
    private TRepository tRepository;
 
    @PersistenceContext
    private EntityManager entityManager;
     
    @Test
    public void insert() {
        TestModel testModel = new TestModel();
        testModel.setId(Long.valueOf(123));
        testModel.setName("test");
        tRepository.create(testModel);
        entityManager.flush();
    }
 
    @Test
    public void select() {
        assertThat(tRepository.get(Long.valueOf(123)), is("test"));
    }
}
cs

- EntityManager 이용

- entityManager의 flush()를 이용해 명시적으로 sql 쿼리문을 실행

- EntityManager를 사용하기 위해 EntityManagerFactory를 Bean으로 등록해줘야함


Spring MVC Test


- Controller에 대한 Test

- 해당 controller에 요청을 보내고 응답 값을 검증


MockMvc


- 웹 어플리케이션을 서버에 배포하지 않은 채로 MVC의 동작을 재현할 수 있는 클래스


1. DI Container 연계모드

- DI Container를 생성하여 실제 spring mvc 동작을 재현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
public class TestSampleController {
    @Autowired
    private WebApplicationContext webApplicationContext;
     
    MockMvc mockMvc;
     
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.build();
    }
}
cs

- Test 시, 사용 할 Application context를 mockmvc에 설정


2. 단독 모드

- Spring Test가 생성된 DI Container를 사용해 spring mvc 동작을 재현

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
public class TestSampleController1 {
    MockMvc mockMvc;
 
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders
.standaloneSetup(new SampleController())
.build();
    }
}
cs

- standaloneSetup method로 test하고 싶은 controller 등록


3. Servlet Filter

- Servlet Filter를 MockMvc에도 추가하여 사용할 수 있음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
public class TestSampleController {
    @Autowired
    private WebApplicationContext webApplicationContext;
 
    MockMvc mockMvc;
 
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders
                    .webAppContextSetup(webApplicationContext)
                    .addFilter(new CharacterEncodingFilter("UTF-8"))   
                    .build();
    }
}
cs

- MockMvcBuilders의 addFilter를 사용하여 servlet filter 추가


Controller Test


1. Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping(value = "/sample")
public class SampleController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String getSample() {
        return "sample";
    }
 
    @RequestMapping(value = "/", method = RequestMethod.POST)
    public TestModel postSample(@RequestBody TestModel testModel) {
       return testModel;
    }
}
cs

- "/sample/"으로 GET 요청 시, "sample" String 반환

- "/sample/"으로 POST 요청 시, 요청 값에 있는 testModel 반환


2. GET/POST Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebMvcConfig.class, TestAppConfig.class})
public class TestSampleController {
    @Autowired
    private WebApplicationContext webApplicationContext;
 
    @Autowired
    ObjectMapper objectMapper;
 
    MockMvc mockMvc;
 
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders
                    .webAppContextSetup(webApplicationContext)
                    .addFilter(new CharacterEncodingFilter("UTF-8"))
                    .build();
    }
 
    @Test
    public void getTest() throws Exception {
        mockMvc.perform(get("/sample/"))
                .andExpect(status().isOk())
                .andExpect(content()
                    .string(objectMapper
                        .writeValueAsString("sample")));
    }
 
    @Test
    public void postTest() throws Exception {
        TestModel testModel = new TestModel();
        testModel.setId(Long.valueOf(123));
        testModel.setName("test");
        mockMvc.perform(post("/sample/")
                   .content(objectMapper
                        .writeValueAsString(testModel))
                   .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content()
                   .string(objectMapper
                        .writeValueAsString(testModel)));
    }
}
cs


1) GET 요청 Test

- "/sample/"로 get 요청을 수행

- andExpect로 응답 값이 200 OK가 오는지 검증

- andExpect로 응답 값의 body가 "sample"이 오는지 검증

- 결과(성공)

15

- 결과(실패)

16


2) POST 요청 test

- "/sample/"로 post 요청을 수행

- request body에 testModel을 Json으로 변환한 값을 넣어주고 content-type에 "application/json"을 넣어줌

- andExpect로 응답 값이 200 OK가 오는지 검증

- andExpect로 응답 값의 body가 보냈던 testModel이 오는지 확인

- 결과(성공)

17

- 결과(실패)

18


Comments