테스트를 정교하게 해야 함

테스트 주도 개발
반복 테스트를 이용한 sw 방법론
작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복 구현 => 애자일 방법론 중 XP의 test-first 개념에 기반을 둠




높은 안정성, 재설계 시간 단축, 디버깅 시간 단축 / 개발 시간 늘어남
clean code that works를 목표로 하기에 테스트 목적으로만 TDD를 하면 안된다.
input과 output에 해당하는 조건을 한개씩 테스트를 먼저 만들어 둠 -> 실패한 것에 대한 api만 수정해줌. 모듈 느낌인 듯 = 최소 품질
유닛 테스트 - 조건 / 통합 테스트 - 기능 전체
4개월 -> 실제 개발 가능 기간은 1개월(3주는 설계, 1주 개발), 나머지 수정(애자일 방식)
시간이 2~3배 증가할 수 있음 => 테스트만 해주는 회사에게 맡김
초기 개발 속도는 느리지만, 전체 프로젝트 수명 주기로 보면 시간 절약. 보통 1.5배 정도 더 걸림. 하지만 결함 발생률이 40~90% 감소
TDD 적용 결과 관련 논문
TDD란
TDD 방법론
실전TDD
src/test/java에 위치
package com.mycom.myapp.basic;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // @Order annotation 사용 가능
public class TestBasic {
@Test // 메소드를 junit으로 테스트
@Order(2)
void test1() {
System.out.println("test1()");
}
@Test
@Order(1)
void test2() {
System.out.println("test2()");
}
@Test
@DisplayName("3rd test")
@Order(4)
void test3() {
System.out.println("test3()");
}
@Test
@DisplayName("회원 수정 테스트")
@Order(3) // 3번째 실행되면서 NullPointer 발생 -> 그래도 다음 테스트는 진행됨
void test4() {
String s = null;
s.length();
System.out.println("test4()");
}
// @BeforeAll, @AfterAll - static method
@BeforeAll
static void beforeAll() {
System.out.println("beforeAll");
}
@AfterAll
static void AfterAll() {
System.out.println("AfterAll");
}
// @BeforeEach, @AfterEach
@BeforeEach // 모든 개별 테스트 메소드 호출 전에 실행
void BeforeEach() {
System.out.println("BeforeEach");
}
@AfterEach
void AfterEach() {
System.out.println("AfterEach");
}
}
요약>
@Order annotation 사용을 위헤
-> @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
메소드를 junit으로 테스트
-> @Test
결과를 함수가 아니라 원하는 이름으로 보기
-> @DisplayName("~~")
테스트끼리 독립적이기에
순서 중간에 에러가 나도 다음 순서 진행됨
@BeforeAll, @AfterAll - static method
= 전체 시작 전과 끝
@BeforeEach, @AfterEach
= 모든 개별 테스트 메소드 호출 전, 후에 실행
assertEqual를 많이 사용
package com.mycom.myapp.basic;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // @Order annotation 사용 가능
public class TestAssert1 {
int getA() {return 6;}
int getB() {return 5;}
@Test // 메소드를 junit으로 테스트
@Order(1)
void test1() {
int a = getA();
int b = getB();
// System.out.println(a);
// 실패하면 error가 아닌 failure로 계산
// "a==b" failure trace에 표시됨
assertEquals(a, b, "a==b");
}
@Test // 메소드를 junit으로 테스트
@Order(2)
void test2() {
assertNotEquals(getA(), getB(), "a!=b");
}
boolean getResult() { return true; }
boolean isTrue = false;
@Test // 메소드를 junit으로 테스트
@Order(3)
void test3() {
// assertEquals(isTrue, getResult(), "getResult is true");
assertTrue(getResult(), "getResult is true");
}
@Test // 메소드를 junit으로 테스트
@Order(4)
void test4() {
assertFalse(getResult(), "getResult is false");
}
@Test // 메소드를 junit으로 테스트
@Order(5)
void test5() {
assertNull(getResult(), "getResult is null");
}
@Test // 메소드를 junit으로 테스트
@Order(6)
void test6() {
assertNotNull(getResult(), "getResult is not null");
}
}
package com.mycom.myapp.basic;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestAssert2 {
int getStringLength(String str) {return str.length();}
@Test
@Order(1)
void test1() {
// String str = "hello";
String str = null;
// 람다로 사용해야 함
assertThrows(NullPointerException.class, () -> getStringLength(str), "NullPointerException throws");
}
// 묶음 테스트
int result = 0;
int m1() {return 4;}
boolean m2() {return false;}
String m3() {return "hello";}
@Test
@Order(2)
void test2() {
// 관련성이 있는 테스트를 한꺼번에 묶어서
// 순서대로 호출해서 정확한 결과가 나오는지 테스트 가능 result 변수 예시
// 전체가 성공해야 성공 하나라도 실패하면, fail
assertAll("묶음 테스트",
() -> assertEquals(4, m1()),
() -> assertTrue(m2()),
() -> assertNotNull(m3()),
() -> assertEquals(result, 0) );
}
int[] expectedArray = {1,2,3};
int[] actualArray = {1,2,3, 4};
@Test
@Order(3)
void test3() {
// 배열 값까지 동일해야 함
assertArrayEquals(expectedArray, actualArray, "두 정수 배열이 같다");
}
// collection
List<String> expectedList = List.of("abc", "def");
List<String> actualList = List.of("abc", "def");
@Test
@Order(4)
void test4() {
// 항목 값까지 동일해야 함
assertIterableEquals(expectedList, actualList, "두 문자열이 같다");
}
// 객체 비교
@Test
@Order(5)
void test5() {
String str1 = "hello";
String str2 = str1; // 같은 객체 참조
String str3 = new String("hello");
// 객체의 주소가 동일해야 함. 값이 같아도 fail
// assertSame(str1, str2, "두 객체는 같다");
// assertSame(str1, str3, "두 객체는 같다"); // == 연산자 비교
assertEquals(str1, str3, "두 객체는 같다");
}
void testBL() {
System.out.println("testBL()");
try {
Thread.sleep(3000);
} catch(Exception e) {
e.printStackTrace();
}
}
@Test
@Order(6)
void test6() {
assertTimeout(Duration.ofSeconds(1), () -> testBL(), "1초 미만 수행 테스트");
}
}

응답 시간 테스트 내용도 계약서에 들어가기에 이를 잘 알고 있어야 함
요약>
assert함수
- assertEquals(), assertNotEquals()
- assertTrue(), assertFalse()
- assertNull(), assertNotNull()
람다식 사용)
예외 처리
- assertThrows()
묶음 처리
- assertAll()
배열, collections, 객체 비교
assertArrayEquals(), assertIterableEquals(), assertSame()
시간 확인
assertTimeout()
package com.mycom.myapp.webapp.springboottest;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.mycom.myapp.user.controller.UserController;
import com.mycom.myapp.user.dto.UserResultDto;
import com.mycom.myapp.user.entity.User;
import com.mycom.myapp.user.repository.UserRepository;
import com.mycom.myapp.user.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
//spring web apllication 코드 테스트
//src/main/java/com/my/com/myapp 이라 코드들 테스트
//controller가 http request, response 동작하는 환경
//모든 코드를 테스트 할 수 있지만, 느리다 => hw의 발전으로 이를 더 많이 사용
// console에서 spring boot web application을 위한 내장 톰캣이 동작하고 테스트 코드가 진행 후, 저장 종료 확인
@SpringBootTest // 모든 스프링 부티 web application을 사용(톰캣의 내장엔진 동작 등등)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j // log 남기기 / spring boot의 기본 설정된 logger 객체 자동 생성, 테스트 코드 및 기본 개발 코드에서도 사용 가능
public class DITest {
@Autowired
UserController userController;
@Autowired
UserService userService;
@Autowired
UserRepository userRepository;
@Test
@Order(1)
void testDI() {
log.info("testDI() start" );
assertNotNull(userController);
assertNotNull(userService);
assertNotNull(userRepository);
log.info("testDI() end" );
}
@Test
@Order(2)
void testDIAll() {
log.debug("testDIAll() start" );
assertAll("DI 묶음 테스트",
() -> assertNotNull(userController),
() -> assertNotNull(userService),
() -> assertNotNull(userRepository) );
log.debug("testDIAll() end" );
}
@Autowired
HttpSession session;
@Autowired
HttpServletRequest request;
@Test
@Order(3)
void testDISessionRequest() {
log.info("testDISessionRequest() start" );
assertAll("DI 묶음 테스트",
() -> assertNotNull(session),
() -> assertNotNull(request) );
log.info("testDISessionRequest() end" );
}
// userController insertUser() test
@Test
@Order(4)
void testDIInsertUser() {
log.info("testDIInsertUser() start" );
User user = new User();
user.setName("test");
user.setPassword("test");
user.setEmail("test");
// 한개만 테스트
// assertNotNull(userController.insertUser(user)); // userResultDto 객체가 리턴되는지 확인
// assertEquals("succes", userController.insertUser(user).getResult());
// 2개를 함께 테스트
UserResultDto userResultDto = userController.insertUser(user);
assertNotNull(userResultDto);
assertEquals("succes", userResultDto.getResult());
log.info("testDIInsertUser() end" );
}
}
요약>
@SpringBootTest
- 모든 코드를 테스트 할 수 있지만, 느리다
@Slf4j
- spring boot의 기본 설정된 logger 객체 자동 생성, 테스트 코드 및 기본 개발 코드에서도 사용 가능
-> log.info(), log.debug()
생성자 주입을 위해 @Autowired 붙이기
package com.mycom.myapp.webapp.webmvctest;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Order;
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 com.mycom.myapp.user.controller.UserController;
import com.mycom.myapp.user.dto.UserResultDto;
import com.mycom.myapp.user.entity.User;
import com.mycom.myapp.user.repository.UserRepository;
import com.mycom.myapp.user.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
// spring web apllication 코드 테스트
// src/main/java/com/my/com/myapp 중 controller가 대상
// 컨트롤러를 제외한 코드는 가상의 코드를 만들어서 테스트 진행
// 일부 코드만 테스트 할 수 있지만, 빠름
// @WebMvcTest에 Controller.class를 명시하지 않으면 전체 DI 대상
@WebMvcTest(UserController.class)
@Slf4j
public class DITest {
@Autowired
MockMvc mocMvc; // 참조하는 객체를 가짜(모형)로 만들어 준디.
@MockBean
UserController userController;
@MockBean
UserService userService;
@MockBean
UserRepository userRepository;
@Test
@Order(1)
void testDI() {
log.info("testDI() start" );
assertNotNull(userController);
assertNotNull(userService);
assertNotNull(userRepository);
log.info("testDI() end" );
}
@Test
@Order(2)
void testDIAll() {
log.debug("testDIAll() start" );
assertAll("DI 묶음 테스트",
() -> assertNotNull(userController),
() -> assertNotNull(userService),
() -> assertNotNull(userRepository) );
log.debug("testDIAll() end" );
}
@Autowired
HttpSession session;
@Autowired
HttpServletRequest request;
@Test
@Order(3)
void testDISessionRequest() {
log.info("testDISessionRequest() start" );
assertAll("DI 묶음 테스트",
() -> assertNotNull(session),
() -> assertNotNull(request) );
log.info("testDISessionRequest() end" );
}
// userController insertUser() test
@Test
@Order(4)
void testDIInsertUser() {
log.info("testDIInsertUser() start" );
User user = new User();
user.setName("test");
user.setPassword("test");
user.setEmail("test");
// 한개만 테스트
// assertNotNull(userController.insertUser(user)); // userResultDto 객체가 리턴되는지 확인
// assertEquals("succes", userController.insertUser(user).getResult());
// 2개를 함께 테스트
// @SpringBootTest와 달리 @WebMvcTest + @MockMvc를 이용한 테스트는 실패(가짜 객체)
UserResultDto userResultDto = userController.insertUser(user);
assertNotNull(userResultDto);
assertEquals("succes", userResultDto.getResult());
log.info("testDIInsertUser() end" );
}
}
package com.mycom.myapp.test.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.mycom.myapp.test.dto.TestDto;
import com.mycom.myapp.test.dto.TestResultDto;
import lombok.extern.slf4j.Slf4j;
// controller 코드를 눈으로 보는 테스트는 브라우저와 postman 등을 이용
// controller 코드만을 대상으로 @WebMvcTest는 코드로 테스트
// service layer와 별도로 client - controller 테스트
@RestController
@Slf4j
public class TestController {
@GetMapping("/hello")
public void m1() {
log.info("/hello");
}
@GetMapping("/param1")
public void m2(@RequestParam("id") Integer id, @RequestParam("name") String name) {
log.info("/param1");
log.info("id:"+id+" name:"+name);
}
@PostMapping("/param2")
public void m3(TestDto testDto) {
log.info("/param2");
log.info(testDto.toString());
}
// 응답
// 단순 문자열
@PostMapping("/response1")
public String m4(TestDto testDto) {
log.info("/response1");
log.info(testDto.toString());
return "success";
}
// json
@PostMapping("/response2")
public TestResultDto m5(TestDto testDto) {
log.info("/response2");
log.info(testDto.toString());
TestResultDto testResultDto = new TestResultDto();
testResultDto.setResult("success");
return testResultDto;
}
}
package com.mycom.myapp.webapp.webmvctest;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.mycom.myapp.test.controller.TestController;
@WebMvcTest(TestController.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
// controller에 집중, 동작 테스트
public class TestControllerTest {
@Autowired
MockMvc mockMvc;
@Test
@Order(1)
void test1() throws Exception{
this.mockMvc
.perform(get("/hello"))
.andExpect(status().isOk());
}
@Test
@Order(2)
void test2() throws Exception{
this.mockMvc
.perform(get("/param1")
.param("id", "123")
.param("name", "hong"))
.andExpect(status().isOk());
}
// MockMvc의 오류를 포함한 응답 메시지 확인 ex. get -> post
@Test
@Order(3)
void test3() throws Exception{
this.mockMvc
.perform(post("/param2")
.param("id", "123")
.param("name", "hong"))
.andExpect(status().isOk());
}
@Test
@Order(4)
void test4() throws Exception{
this.mockMvc
.perform(post("/response1")
.param("id", "123")
.param("name", "hong"))
.andExpect(status().isOk())
.andExpect(content().string("success")); // import 주의
}
@Test
@Order(5)
void test5() throws Exception{
this.mockMvc
.perform(post("/response2")
.param("id", "123")
.param("name", "hong"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) // controller의 응답에 application/json 확인
.andExpect(jsonPath("$.result").value("success")); // json 응답 중 result 항목 ($.result)의 값을 확인
}
}
junit test coverage는 전체 코드 대비 몇프로 하는지를 보여줌. 품질 중 하나
요약>
@WebMvcTest(~Controller.class)
- 컨트롤러를 제외한 코드는 가상의 코드를 만들어서 테스트 진행
- 일부 코드만 테스트 할 수 있지만, 빠름
MockMvc
- 참조하는 객체를 가짜(모형)로 만들어 줌
결과와 dto를 한 번에 테스트 하려면 ResultDto로 테스트
경로 테스트
.perform(방식("경로"))
.andExpect(status().isOk())
- HTTP 상태 확인(200이면 통과)
.andExpect(content().string("~~"))
- 응답 본문 내용 확인; import 주의
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
- controller의 응답에 application/json 확인
.andExpect(jsonPath("$.result").value("~~"))
- json 데이터 확인
@SpringBootTest vs @WebMvcTest
- 통합테스트 vs 단위 테스트
- 실체 객체 vs 가짜 객체
- @SpringBootTest는 실제 db와 연결이 되지만, @WebMvcTest는 안됨
- 느림 vs 빠름 (10~100배 차이)
- @SpringBootTest는 전체적인 로직과 DB 연결성 검증을 위한 것이고
@WebMvcTest는 API 요청/응답, 유효성 검사 등 컨트롤러 로직 검증
=> 개발 중에는 @WebMvcTest 사용, 배포 전에는 @SpringBootTest를 사용