12/2 TDD방식, JUnit

AI·2025년 12월 2일

테스트를 정교하게 해야 함

TDD(Test Driven Develpment)

테스트 주도 개발
반복 테스트를 이용한 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

JUnit

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
= 모든 개별 테스트 메소드 호출 전, 후에 실행

assert

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()

@SpringBootTest

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 붙이기

@WebMvcTest

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" );
	}

}

testController

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;
	}
}

test

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를 사용

0개의 댓글