[Section3] Spring MVC - 테스팅(Testing)
2023. 6. 28. 23:34
🧑🏻💻 TIL(Today I Learned)
✔️ 단위 테스트, JUnit
💡 테스트(Test)
테스트를 해야 되는 이유?
: 테스트를 제대로 잘 거쳐서 데스트 대상이 검증 과정에 잘 통과하게 만들어 최대한 더 나은 결과를 얻기 위해서
➡️ 우리는 이미 학습을 진행하면서 애플리케이션에 대한 테스트를 진행하고 있었음
➡️ 어떻게? 예제 코드를 직접 타이핑하고 애플리케이션을 실행하고 Postman으로 HTTP 요청을 전송해서 기대하던 JSON 응답 결과가 출력되는지 확인하는 것으로 (Java 기반의 애플리케이션 테스트)
➡️ 하지만 애플리케이션 전체가 아니라 각 계층별로 따로 테스트를 하고 싶을 때, 비즈니스 로직에서 구현한 특정 메서드만 테스트하고 싶을 때는 위와 같이 테스트하는 것은 비효율적이고 번거로운 상황 발생
⭐️ 위와 같은 문제 때문에 Java에서는 메서드 같은 아주 작은 단위를 가지는 기능들을 테스트할 수 있는 방법이 존재하고 Spring에서는 계층별로 테스트할 수 있는 테스트 기법을 지원하고 있음 ⭐️
✔️ 그중에서 아주 작은 단위의 기능을 테스트하는 방법이 "단위 테스트"
🔎 단위 테스트(Unit Test)란?
- 기능 테스트
: 테스트 범위가 제일 큼, 즉 단위가 가장 큼
: 애플리케이션을 사용하는 사용자 입장에서 애플리케이션이 제공하는 기능이 올바르게 동작하는지 테스트
: 개발한 애플리케이션과 연관된 대상이 많아서 단위 테스트라고 부르기는 힘듦 - 통합 테스트
: 클라이언트 측 툴 없이 개발자가 짜 놓은 테스트 코드를 실행시켜서 이루어지는 경우가 많음
: 애플리케이션의 여러 계층이 연관되어 있어 독립적인 테스트가 가능하다고 볼 수 없기 때문에 단위 테스트 X - 슬라이스 테스트
: 애플리케이션을 특정 계층으로 쪼개어서 하는 테스트
: 하지만 여전히 단위 테스트라고 부르기에는 단위가 큰 테스트, 즉 단위 테스트 X
: 애플리케이션의 일부만 테스트하기 때문에 부분 통합 테스트라고 부르기도 함 - 단위 테스트
: 서비스 계층에서 핵심 로직 즉, 비즈니스 로직에서 사용하는 클래스들이 독립적으로 테스트하기 가장 좋은 대상이기 때문에 단위 테스트라고 부름
: 단위 테스트 코드는 메서드 단위로 대부분 작성됨
: 일반적으로 단위 테스트는 최대한 독립적인 것이 좋고 최대한 작은 단위인 것이 더 좋음
(더 작은 단위일수록 다른 연관된 기능들을 생각할 필요도 없고 테스트 코드 짜기도 더 단순해지고 빠르게 테스트할 수 있기 때문)
테스트 케이스(Test Case)?
: 테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세
: 즉 메서드 등 하나의 단위를 테스트하기 위해 작성하는 테스트 코드
🔎 단위 테스트를 위한 F.I.R.S.T 원칙
- Fast(빠르게)
: 작성한 테스트 케이스는 빨라야 한다. - Independent(독립적으로)
: 각각 테스트 케이스는 독립적이어야 한다.
: 어떤 테스트 케이스를 먼저 실행시켜도 실행되는 순서와 상관없이 정상적인 실행이 보장되어야 한다는 것 - Repeatable(반복 가능하도록)
: 어떤 환경에서도 반복해서 실행이 가능해야 한다.
: 외부 서비스나 외부 리소스가 연동되는 경우 앞서 언급한 원칙들을 포함해서 동일한 테스트 결과를 보장하지 못하기 때문에 외부 서비스나 리소스의 연동을 끊어주는 것이 좋음 - Self-Validating(셀프 검증이 되도록)
: 테스트 케이스 스스로 결과가 옳은지 그른지 판단할 수 있어야 한다는 것 - Timely(시기적절하게)
: 테스트하려는 기능 구현을 하기 진적에 단위 테스트를 작성해야 한다는 의미
: 구현하고자 하는 기능을 단계적으로 업그레이드하면서 그때그때 테스트 케이스를 단계적을 업그레이드하는 방식이 좋음
🔎 JUnit 없이 비즈니스 로직에 단위 테스트 적용해 보기
➡️ 단위 테스트를 제일 쉽고 빠르게 적용할 수 있는 부분은 헬퍼(helper) 클래스 또는 유틸리티(utility) 클래스
public class StampCalculator {
public static int calculateStampCount(int nowCount, int earned) {
return nowCount + earned;
}
}
- 회원이 주문한 커피 수량만큼 획득한 스탬프 수를 더해서 누적 스탬프 수를 계산하는 헬퍼 클래스
-> 테스트 대상인 헬퍼 클래스
public class StampCaculatorTestWithoutJUnit { // JUnit을 사용하지 않은 단위 테스트
public static void main(String[] args) {
calculateStampCountTest();
}
private static void calculateStampCountTest() {
//given
int nowCount = 5;
int earned = 3;
//when --> 테스트 대상
int actual = StampCalculator.calculateStampCount(nowCount, earned);
int expected = 7;
//then
System.out.println(expected == actual);
}
}
- StampCalculator의 calculatorStampCount() 메서드가 잘 동작하는지 검증하는 테스트 케이스
✔️ Given-When-Then 표현 스타일
➡️ BDD(Behavier Driven Development) 테스트 방식에서 사용하는 용어
- Given
- 테스트를 위한 준비 과정 명시
- 테스트에 필요한 전제 조건 포함
- 테스트 대상에 전달되는 입력 값(테스트 데이터) 또한 포함
- When
- 테스트할 동작(대상) 지정
- 단위 테스트에서는 일반적으로 메서드 호출을 통해 테스트를 진행하므로 한 두줄 정도로 작성이 끝나는 부분
- Then
- 테스트의 결과를 검증하는 영역
- 일반적으로 예상하는 값(expected)과 테스트 대상 메서드의 동작 수행 결과(actual) 값을 비교해서 기대한 대로 동작을 수행하는지 검증(Assertion)하는 코드들이 포함됨
Assertion?
: 테스트 결과를 검증할 때 주로 사용하는 용어
: 예상하는 결과 값이 참이길 바라는 것이라고 이해하면 좋음
💡 JUnit?
➡️ Java 언어로 만들어진 애플리케이션을 테스트하기 위한 오픈 소스 테스트 프레임워크
(사실상 Java의 표준 프레임워크라고 해도 과언이 아님)
Spring Boot Initializr 에서 Gradle 기반의 Spring Boot 프로젝트를 생성하고 오픈하면 기본적으로 "src/test" 디렉토리 만들어짐
기본적으로 testImplementation >'org.springframework.boot:spring-boot-starter-test' 스타터가 포함되며, JUnit도 포함됨
✍🏻 JUnit을 사용한 테스트 케이스의 기본 구조
import org.juint.jupiter.api.Test;
public class JunitDefaultStructure {
// (1)
@Test
public void test1() {
// 테스트하고자 하는 대상에 대한 로직 작성
}
// (2)
@Test
public void test2() {
// 테스트하고자 하는 대상에 대한 로직 작성
}
// (3)
@Test
public void test3() {
// 테스트하고자 하는 대상에 대한 로직 작성
}
✔️ Assertion 메서드 사용하기
➡️ Assertion? : 예상하는 결과 값이 참이길 바라는 논리적인 표현, "검증한다"
➡️ JUnit에서는 이 Assertion와 관련된 메서드를 통해 테스트 대상에 대한 검증을 진행할 수 있음
- assertEquals() : 기대하는 값과 실제 결과 값이 같은지 검증 가능
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HelloWorldTest {
@DisplayName("Hello World Test") // (1)
@Test
public void assertionTest() {
String expected = "Hello, World";
String actual = "Hello, World";
assertEquals(expected, actual); // (2)
}
}
- 실행시키면 "passed"
- 만약 expected 값을 "Hello JUnit" 이렇게 바꾸면 테스트 실행 결과가 failed 로 나타남
- assertNotNull()
: 대상 객체가 null 인지 아닌지 알 수 있음
: assertNotNull() 메서드의 첫 번째 파라미터는 테스트 대상 객체, 두 번째 파라미터는 테스트에 실패했을 때 표시할 메세지
import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class AssertionNotNullTest {
@DisplayName("AssertionNull() Test")
@Test
public void assertNotNullTest() {
String currencyName = getCryptoCurrency("ETH");
// (1)
assertNotNull(currencyName, "should be not null");
}
private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit);
}
}
- map 안에 "ETH" 이름이 저장되어 있으면 "passed", 이름이 없으면 "should be not null" 출력
- assertThrows()
: 호출한 메서드의 동작 과정 중에 예외가 발생하는지 테스트
: 첫 번째 파라미터에는 발생이 기대되는 예외 클래스 입력, 두 번째 파라미터인 람다 표현식에는 테스트 대상 메서드 호출
import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class AssertionExceptionTest {
@DisplayName("throws NullPointerException when map.get()")
@Test
public void assertionThrowExceptionTest() {
// (1)
assertThrows(NullPointerException.class, () -> getCryptoCurrency("XRP"));
}
private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit).toUpperCase();
}
}
※ Assertion 메서드를 더 보고 싶다면
https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Assertions.html
'SEB_BE_45 > 공부 정리' 카테고리의 다른 글
[Section3] Spring MVC - Testing 3 (0) | 2023.07.02 |
---|---|
[Section3] Spring MVC - Testing 2 (0) | 2023.07.02 |
[Section3] Spring MVC - 트랜잭션(Transaction) (0) | 2023.06.26 |
[Section3] Spring MVC - JPA 기반 액세스 계층 (0) | 2023.06.21 |
[Section3] Spring MVC - JDBC 기반 데이터 액세스 실습 (0) | 2023.06.21 |