[ TDD란 ]

Test Driven Development 의 약자로 테스트 코드를 작성하고 프로덕션 코드를 개발하는, 테스트에서 부터 개발이 이뤄지는 테스트가 주도하는 개발 방법.  

TDD = TFD(Test First Development) + 리팩토링

 

[ TDD 원칙 ]

실패하는 단위 테스트를 작성할 때 까지 프로덕션 코드를 작성하지 않는다.

컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성.

현재 실패하는 테스트를 통과할 정도로만 실제 코드 작성.

 

[ TDD 는 어떻게 해야하나 ]

요구사항 분석을 통한 대략적인 설계를 한 후 객체를 추출

UI, DB 등과 의존관계를 가지지 않는 핵심 도메인 영역을 집중 설계

Controller , View 보단 우선적으로 Domain(Model) 영역을 1차적으로 단위테스트 작성

 

 

 

반응형

'back' 카테고리의 다른 글

[Java] 일급콜렉션 : First Class Collection  (0) 2022.10.06
JWT  (0) 2020.11.18
[gradle] jar build, war build  (0) 2020.03.25

스프링프레임워크에서 JUnit 을 이용한 API 테스트 : Spring Framework + JUnit + JNDI

junit 없이 매번 was 재기동 및 postman 호출로 api 결과들을 확인하는 방식으로 테스트를 하다 

junit 을 한 번 사용해 볼까 하여 기존 프로젝트에 설정을 잡고 사용해보았다.

 

혹여 본 포스팅 내용을 참고 하신다면, 프로젝트마다 context 및 프로젝트내에서 사용중인 lib version 등의 설정이 다르므로 제가 작성한 글은 그냥 참고만 하시길 바랍니다. 

실제 프로젝트에서 사용중인 코드 내에서 일부를 발췌하였으므로 github 등에 소스 공유는 힘듭니다.

언제가 될진 모르겠지만 별도의 프로젝트를 만들어 추후 github에 올려놓고 repository 링크를 공유하겠습니다..

출하면 안될 것 같은 패키지 경로, 실제 소스, 결과 등의 코드는 생략하였습니다.

 

[pom.xml]

필요한 의존성 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.9.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>
cs

 

[ApiControllerTest.java]

src/test/java 밑에 패키지 생성 후 아래와 같이 작성한다.

※ 테스트 케이스 생성 방법

테스트를 하고자 하는 클래스 선택 -> new -> JUnit Test Case

실제 소스 경로와 동일한 경로의 테스트 소스 경로가 만들어진다.

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package ~;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import java.nio.charset.Charset;
import java.util.Map;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import ~.BaseController;
import ~.IdolApiController;
import ~.IdolApiService;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
                  "classpath:bmp/spring/context-*.xml" 
                , "classpath:bmp/sqlmap/mybatis-config.xml" 
                , "classpath:bmp/servlet/servlet-*.xml"
              
                })
@ActiveProfiles("dev")
@TransactionConfiguration(transactionManager="txManager", defaultRollback = false)
@Transactional
@WebAppConfiguration
public class ApiControllerTest3 {
    protected static final Logger logger = LoggerFactory.getLogger(ApiControllerTest3.class);
    
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;
    
    private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
    
    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
    }
    
    
    @Test
    public void testIdolApiController() throws Exception {
        
        JSONObject jobj = new JSONObject();
        jobj.put("id""test");
        jobj.put("clientIp""172.0.0.1");
        jobj.put("content""testcontent.mpg");
        String p = jobj.toString();
        
        MvcResult rs = null;
        
        rs = mvc.perform(post("/getdata")
                                       //.content("{\"said\":\"test\", \"clientIp\":\"172.0.0.1\", \"content\":\"testcontent.mpg\"}")
                                         .content(jobj.toString())
                                         .contentType(contentType)
                        )
        .andDo(print())
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andReturn();
        
        logger.debug("---------------------------");
        logger.debug("result : {}", rs.getModelAndView().getModel());
        
    }
}
 
cs

31 L : junit test 를 위해 클래스에 붙여주는 annotation

32 L : 참조할 bean 들의 설정이 잡혀있는 context 경로 (classpath/file 및 절대/상대 경로 주의)

38 L : 활성시킬 profile 지정 (profile과 관련된 내용은 여기를 참고)

39 L : 트랜잭션 설정

40 L : 트랜잭션 적용

41 L : web controller 테스트를 위한 annotation

53 L : @Before를 메소드에 달아줄 경우 @Test 실행 전에 실행된다.

55 L : MockMvc 빌드를 위한 코드 ( 정확히 모르니 추후 Mock 관련 공부를 하며 보충 )

70~77 L : 

post(url) : post 방식 호출

content(String) : body 로 전송

contentType(contentType) : 호출시 content Type 지정

andDo(print()) : 결과 출력

andReturn() : 결과값 리턴(MvcResult rs 변수에 담기)

 

※ context 가 아닌 특정 controller 만 주입 받아 처리하고 싶은 경우 아래와 같이..

참고 : https://thswave.github.io/java/2015/03/02/spring-mvc-test.html

@Mock
VodApiService vodApiService;
@InjectMocks
VodApiController vodApiController;
private MockMvc mvc;

@Before
public void setUp() throws Exception {
      MockitoAnnotations.initMocks(this);
      mvc = MockMvcBuilders.standaloneSetup(vodApiController).build();
}

[junit 실행 결과]

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
MockHttpServletRequest:
         HTTP Method = POST
        Request URI = /getData
          Parameters = {}
             Headers = {Content-Type=[application/json;charset=UTF-8]}
 
             Handler:
               Type = ~
             Method = ~
 
  Resolved Exception:
                Type = null
 
        ModelAndView:
           View name = jsonView
                View = null
           Attribute = result_msg
               value = 
           Attribute = data
              value = {~}
           Attribute = result_cd
               value = 1
 
            FlashMap:
 
MockHttpServletResponse:
              Status = 200
       Error message = null
             Headers = {Content-Type=[application/json;charset=UTF-8], Pragma=[no-cache], Cache-Control=[no-cache, no-store, max-age=0], Expires=[1]}
        Content type = application/json
               Body = {~}
       Forwarded URL = null
      Redirected URL = null
             Cookies = []
DEBUG: ~ - ---------------------------
DEBUG: ~ - result : {~}
cs

 

여기서 log가 debug 레벨로 잘 출력 됐음을 확인 할 수 있는데, 이는 log4j 설정이 잡혀 있는 파일 (본 프로젝트에선 log4j2.xml)을 src/test/resources 밑에 복사 붙여넣기 한 후, log 레벨을 debug 로 내렸기 때문이다.

src/main/resources 내의 log4j 설정 파일을 읽기 전에 src/test/resources 내의 log4j 설정 파일을 우선적으로 읽는다. 관련 내용은 여기를 참고.

log4j 설정 관련 글은 여기를 참고.

 

JNDI 를 사용하고 있는 경우, jUnit test

보통은 위와 같이 테스트 할 수 있지만,

JNDI 를 사용하여 DB 설정이 잡혀 있는 경우, 별도의 처리가 필요하다.

JNDI 설정의 경우 was 에 DB 설정 정보가 존재하며 was 구동시 이를 읽어 들이는 방식인데(정확히 모름+공부필요),

was가 아닌 JUnit 구동시 DB설정정보를 읽어들이지 못해 이와 관련된 Bean을 주입 받을 때 에러가 발생하기 때문이다.

이를 해결하는 방법은 아래와 같이 두가지 정도가 있는 듯 하다.

 

[방법 1 : 테스트용 context 별도 설정]

1) JNDI 설정이 잡혀있는 context-datasource.xml 을 복사하여 src/test/resources 경로 밑에 붙여넣은 후 파일명을 test-datasource.xml 로 바꿔준다.

[example]

아래와 같은 JNDI 설정을

1
2
3
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiName" value="java:/jdbc/testJNDI" />
</bean>
cs

아래와 같이 바꿔준다. (was에 있을 DB접속정보(jndi 설정)를 context에 직접 작성)

1
2
3
4
5
6
7
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" >
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/> 
    <property name="url" value="jdbc:mysql://~:3306/~?useSSL=false&amp;serverTimezone=UTC"/> 
    <property name="username" value="~"/> 
    <property name="password" value="~"/>
</bean>
 
cs

2) 위에서 작성한 JUnit테스트 소스인 ApiControllerTest.java 를 아래와 같이 수정한다.

@ContextConfiguration 부분에 선언된 context 에 1~2) 번에서 작성한 context (jndi 설정을 걷어낸 DB설정(test-datasource.xml))를 추가한다.

1
2
3
4
5
6
@ContextConfiguration(locations = {
                  "classpath:bmp/spring/context-*.xml"
              , "classpath:test-*.xml"
                , "classpath:bmp/sqlmap/mybatis-config.xml" 
                , "classpath:bmp/servlet/servlet-*.xml"
                })
cs

 

위에서 언급한 log4j 설정과 마찬가지로, src/test/resources 가 src/main/resources 보다 선행돼서 읽히기 때문에 src/main/resources 밑의 context 에 선언된 Bean 과 동일한 이름의 Bean이 src/test/resources 밑의 context 에 선언돼 있어도 상관이 없는 듯 하다.

 

 

[방법 2 : JNDI 바인딩]

1) SpringJUnit4ClassRunner 를 상속받는 ExtSpringJUnit4ClassRunner 작성.

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
package ~;
 
import org.junit.runners.model.InitializationError;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jndi.JndiTemplate;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
public class ExtSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner {
 
    public ExtSpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
        // TODO Auto-generated constructor stub
        try {
            bindJndi();
        } catch (Exception e) {
            
        }
    }
    
    private void bindJndi() throws Exception {
        SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
        builder.activate();
        
        JndiTemplate jt = new JndiTemplate();
        
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://~:3306/~?useSSL=false&amp;serverTimezone=UTC");
        ds.setUsername("~");
        ds.setPassword("~");
        
        jt.bind("java:/jdbc/testJNDI", ds);
    }
}
 
cs

위와 같이 Jndi 바인딩을 java 소스로 처리.

관련 부분은 여기를 참고했음.

(이런식으로 JNDI 설정을 잡아줄 수 있는 걸 보면 다른 부분 설정도 이와 비슷하게 처리가 가능할 듯)

 

2) 위에서 작성한 JUnit테스트 소스인 ApiControllerTest.java 를 아래와 같이 수정한다.

1
@RunWith(ExtSpringJUnit4ClassRunner.class)
cs

SpringJUnit4ClassRunner 대신 위에서 작성한 ExtSpringJUnit4ClassRunner를 사용한다.

나머지 부분은 동일하다.

(jndi 설정을 java소스(ExtSpringJUnit4ClassRunner.java)에서 잡아주었으므로 방법 1과 같이 별도의 context 를 @ContextConfiguration에 추가해줄 필요가 없다)

 

JUnit 사용시 Maven Build 주의사항

JUnit을 사용하여 테스트를 하고 있고 Maven Build 로 war 파일을 생성하고 있다면 주의해야 할 점.

테스트 코드 결과가 실패인 경우, Maven Build 의 결과가 Failure (실패)로 떨어진다.

이땐 아래와 같이 해결한다.

 

[방법 1 : UI 에서 build option 지정]

프로젝트 우클릭 > Run Configurations.. > 좌측 리스트에서 Maven Build 중 해당 프로젝트 선택 > skip Tests 옵션 체크

 

[방법 2 : pom.xml 에 build option 수정]

1
2
3
4
5
6
7
8
9
 <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.18.1</version>
    <configuration>
        <useSystemClassLoader>false</useSystemClassLoader>
        <skipTests>true</skipTests>
    </configuration>
</plugin>
cs

위에서 7번라인과 같이 skipTests 옵션 true 지정.

해당 내용은 여기를 참고했음.

 

 

전체적인 JUnit에 대한 설명 및 설정등은 여기를 참고했음.

(이것저것 공부하고 관련 레퍼런스 찾아보고 블로그/깃헙 돌아다니다 보면 이상하리만큼 자주 도달하게 되는 곳이 있는데 그중 하나인 곳.. 항상 감사드립니다..)

 

본 포스팅은 다른 포스팅들과 마찬가지로 수시로 수정 및 업데이트 하겠습니다.

 

참고 :

https://effectivesquid.tistory.com/entry/Spring-test-%EC%99%80-Junit4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8

반응형

+ Recent posts