테스트 주도 개발(Test-driven development TDD)은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나입니다. TDD는 다음과 같은 3단계로 이루어져 있고, 이를 계속 반복하는 작업입니다.
TDD의 장점으로는 메소드 단위로 작업하기에 메소드에는 정말 필요한 기능만 정의하게 됩니다. 그리고 불확실한 변수, 메소드, 로직들이 들어가지 않아 클린함수를 구현할 수 있게 됩니다.
우선 아래와 같이 junit과 spring-test를 pom.xml에 dependency를 추가해줍니다.
자바 테스트를 사용하기 위해서는 기본적으로는 juint이 필요합니다. 그리고 Spring MVC 웹 애플리케이션 테이스를 위해서 MockMvc가 필요한데, 이를 제공해주는 spring-test 역시 추가해줍니다.
junit의 경우 4.12 이상을 사용하는 것을 추천하고(여기서는 4.13 사용),
spring-test는 사용하고 있는 스프링 버전에 맞추어 줍니다.
또한 servlet의 경우 @WebAppConfiguration을 사용하기 위해 3.0 이상으로 만들어 줍니다.
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<!-- spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- 서블릿 버전을 3.0이상으로 설정 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
@Test
: 해당 메소드가 테스트 대상 메소드임을 의미, junit 4의 경우 메소드가 public이여야함 (junit 5부터는 public일 필요 없음)
@Test(timeout=10)
: 테스트 메소드 수행 제한 시간이 10 ms
@Test(expected=RuntimeException.class)
: RuntimeException이 발생하면 테스트 성공, 아니면 실패
@Before
: 각 테스트 메소드에서 공통적으로 실행해야 할 작업 포함, 테스트 메소드가 실행되기 전에 실행됨 (junit 5부터는 @BeforeEach
)
@After
: 각 테스트 메소드의 실행이 완료된 후에 동일한 마무리 작업을 수행할 경우 사용, 테스트 메소드가 실행된 후에 실행됨 (junit 5부터는 @AfterEach
)
@BeforeClass
: static 메소드이며, 모든 테스트 메소드가 동작하기 전 공통적으로 실행해야 하는 작업 수행 (junit 5부터는 @BeforeAll
)
@AfterClass
: static 메소드이며, 모든 테스트 메소드가 생행되고 난 후에 한번 실행하는 메소드 (junit 5부터는 @AfterAll
)
Assert는 "확인"을 수행하는 동작입니다.
assertEquals([String message,] expected, actual)
: 두 값이 동일한지 검사한다.assertEquals("메시지", 5, 2 + 3);
assertNotEquals([String message,] unexpected, actual)
: 두 값이 동일하지 않은지 검사합니다.assertNotEquals("메시지", 5, 2 + 2);
assertTrue([String message,] boolean condition)
: 조건이 참인지 검사합니다.assertTrue("메시지", 3 < 5);
assertFalse([String message,] boolean condition)
: 조건이 거짓인지 검사합니다.assertFalse("메시지", 3 > 5);
assertNull([String message,] object)
: 객체가 null인지 검사합니다.assertNull("메시지", null);
assertNotNull([String message,] object)
: 객체가 null이 아닌지 검사합니다.assertNotNull("메시지", new Object());
assertSame([String message,] expected, actual)
: 두 객체가 같은 객체인지 (== 연산자로 비교) 검사합니다.Object obj = new Object();
assertSame("메시지", obj, obj);
assertNotSame([String message,] unexpected, actual)
: 두 객체가 같은 객체가 아닌지 검사합니다.Object obj1 = new Object();
Object obj2 = new Object();
assertNotSame("메시지", obj1, obj2);
assertArrayEquals([String message,] expectedArray, actualArray)
: 배열이 동일한지 검사합니다.int[] expected = {1, 2, 3};
int[] actual = {1, 2, 3};
assertArrayEquals("메시지", expected, actual);
fail([String message])
: 테스트를 실패로 처리합니다. 주로 특정 조건에 도달하면 테스트를 실패시키기 위해 사용됩니다. STS 환경에서 처음 test case 만들시 default로 작성됩니다.wac
)WebApplicationContext
는 spring의 ApplicationContext
를 확장한 것으로, 웹 애플리케이션의 컨텍스트를 제공하는 역할을 합니다. 이는 웹 애플리케이션의 구성, 웹 환경에서의 빈(bean) 관리, 그리고 웹 관련 설정을 포함합니다.
mockMvc
)MockMvc
는 스프링 MVC 애플리케이션의 웹 계층을 테스트하기 위한 유틸리티로, 실제 서버를 구동하지 않고도 MVC 컨트롤러를 테스트할 수 있게 해줍니다. 이는 DispatcherServlet
을 직접 호출하여 요청과 응답을 모킹(mocking)함으로써 작동합니다.
아주 간단한 로그인을 확인해보겠습니다.
@RequiredArgsConstructor
@Controller
public class MemberController {
private final MemberService memberService;
// 아이디체크
@ResponseBody
@PostMapping("login.me")
public String idCheck(Member member) {
int result = memberService.idCheck(member);
if (result > 0) {
return "redirect:/";
} else {
return "redirect:/fail";
}
}
}
그리고 테스트 코드의 경우 다음과 같습니다.
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {
"file:src/test/resources/root-context.xml",
"file:src/test/resources/servlet-context.xml",
"file:src/test/resources/spring-security.xml"
})
public class MemberControllerTest {
@Autowired
private WebApplicationContext wac;// 웹어플리케이션 컨텍스트
private MockMvc mockMvc; // HTTP요청 및 응답을 모의로 테스트 할 수 있는 객체
@Before // Test메서드가 실행되기전에 실행하는 메서드
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); // mockMvc에 컨텍스트 정보를 저장
log.info("===============mockMvc 준비 완료========================");
}
@Test
public void testLoginMember() {
try {
mockMvc.perform(post("/login.me") // POST 메서드로 /login.me 요청
.param("userId", "admin") // 요청 파라미터 userId 세팅
.param("userPwd", "1234"))
.andDo(print()) // 요청한거에대한 응답코드를 console에 출력
.andExpect(status().isFound())// HTTP 상태코드가 302인지 확인 - redirect
.andExpect(redirectedUrl("/")); // 리다이렉트 경로 확인
// .andExpect(status().isOk()); // HTTP 상태코드가 200 인지 확인
} catch (Exception e) {
e.printStackTrace();
}
}
}
응답값이 있어 이를 비교하기 위해서는 MvcResult로 받아서 이를 비교해야합니다.
간단한 아이디체크를 확인해보겠습니다. 실제로그인은 아닙니다
@RequiredArgsConstructor
@Controller
public class MemberController {
private final MemberService memberService;
// 로그인
@ResponseBody
@PostMapping("idCheck.me")
public String idCheck(String checkId) {
int result = memberService.idCheck(checkId);
if (result > 0) {
return "NNNNY";
} else {
return "NNNNN";
}
}
}
그리고 Test 코드의 경우 다음과 같이 작성합니다.
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {
"file:src/test/resources/root-context.xml",
"file:src/test/resources/servlet-context.xml",
"file:src/test/resources/spring-security.xml"
})
public class MemberControllerTest {
@Autowired
private WebApplicationContext wac; // 웹어플리케이션 컨텍스트
private MockMvc mockMvc; // HTTP요청 및 응답을 모의로 테스트 할 수 있는 객체
@Before // Test메서드가 실행되기 전에 실행하는 메서드
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); // mockMvc에 컨텍스트 정보를 저장
log.info("====================mockMvc 준비 완료====================");
}
@Test
public void testIdCheck() {
MvcResult result;
try {
result = mockMvc.perform(
post("/idCheck.me") // POST 메서드로 /login.me 요청
.param("checkId", "admin")) // 요청 파라미터 userId 세팅
.andExpect(status().isOk()) // 200
.andReturn(); //
// 응답 본문의 내용 확인
String content = result.getResponse().getContentAsString();
assertEquals("NNNNY", content); // 아이디 사용가능 결과와 비교
} catch (Exception e) {
e.printStackTrace();
}
}
}