테스팅(Testing)
일반적인 테스트(Test)의 의미는 어떤 대상에 대한 일정 기준을 정해놓고, 그 대상이 정해진 기준에 부합하는지 부합하지 못하는지를 검증하는 과정이라고 말할 수 있습니다.
테스트를 하지 않는다면 어떤 대상에 대한 검증이 정상적으로 이루어지지 않습니다.
검증이 정상적으로 이루어지지 않으면 잘못된 결과가 나올 수 있습니다.
그렇기 때문에 대상이 검증 과정에서 잘 통과되게 만들어 최대한 더 나은 결과를 얻기위해 테스트를 진행해야 합니다.
여기서 ‘최대한 더 나은 결과’라고 한 이유는 모든 테스트가 100 퍼센트 완벽하게 이루어질 수 없기 때문입니다.
테스트를 통해 100%는 아니더라도 최대한 그에 가깝게 오류를 줄이고 원하는 결과를 도출하도록 노력해야합니다.
이러한 테스트를 진행하는 과정을 테스팅(Testing)이라고 합니다.
소프트웨어 개발 관점에서의 테스팅(Testing)은 개발자가 작성한 코드가 예상한 대로 작동하는지 확인하기 위해 자동화된 테스트 케이스를 만들고 실행하는 과정입니다.
이를 통해 코드의 일부분 또는 전체를 실행하고 결과를 검증할 수 있습니다. 테스트는 일련의 입력값을 제공하여 애플리케이션의 동작을 확인하고, 예상한 출력값과 실제 출력값을 비교함으로써 코드의 정확성과 예외 처리 등을 평가합니다.
이전에 Java Spring을 학습하면서 직접 코드를 타이핑하고, Intellij IDE에서 애플리케이션을 실행한 후,
실행시킨 애플리케이션에 Postman으로 HTTP 요청을 전송해서 JSON 응답 결과들을 출력해 봤을것입니다.
이러한 행위도 하나의 테스트라고 볼 수 있습니다.
만약 기대했던 JSON 응답 결과를 Postman으로 확인했다면 테스트에 성공한 것입니다.
반대로 기대했던 JSON 응답 결과가 아닌 에러 응답이 나왔다면 테스트에 실패한 것입니다.
(물론, 에러를 기대하며 테스트를 진행해서 원하는 에러가 나왔다면 예외 테스트에 성공한 것입니다.)
테스트에실패했다면 직접 작성한 코드에서 어디가 문제인지 살펴볼 것이고, 코드상으로 발견하기 어렵다면 콘솔에 출력된 로그를 확인할 수도 있을 것입니다.
그것마저도 안 된다면 코드 라인에 브레이크포인트(breakpoint)를 걸어서 라인 단위로 어디가 잘못인지 확인할 것입니다.
✔지금껏 해왔던 방식보다 더 간편하고 쉬운 애플리케이션 테스트는 없을까요?
매번 수작업으로 애플리케이션을 실행시키고, Postman을 열어서 HTTP 요청을 보낸다는 것은 아무래도 비효율적이고, 불편합니다.
또한, 이러한 과정은 애플리케이션 전체가 아닌 API 계층, 서비스 계층, 데이터 액세스 계층 중에서 하나의 계층만 테스트하고 싶은 경우 한 계층만 테스트하기 쉽지 않습니다.
비즈니스 로직에서 구현한 특정 메서드만 테스트하고 싶을 때 애플리케이션 실행, Postman 툴 실행 및 요청이라는 과정을 해야하는 비효율적인 상황도 발생합니다.
이 경우 Java에서는 메서드 같은 아주 작은 단위를 가지는 기능을 테스트할 수 있는 방법이 있습니다.
Spring에서는 계층별로 테스트할 수 있는 테스트 기법 역시 지원을 해주고 있습니다.
테스트 유형
테스트에는 기능 테스트(Functional Test), 통합 테스트(Integration Test), 슬라이스 테스트(Slice Test), 단위 테스트(Unit Test)라는 여러 가지 유형이 있습니다.
테스트 범위가 큰 순서대로 설명하겠습니다.
- 기능 테스트
기능 테스트는 애플리케이션의 전체기능을 사용자의 관점에서 올바르게 동작하는지를 테스트하는 것입니다.
이 테스트는 애플리케이션을 완전히 실행하고, 사용자가 실제로 사용하는 것과 같은 동작을 수행하여 애플리케이션의 기능을 확인합니다.
기능 테스트는 사용자 인터페이스(UI)나 API를 통해 애플리케이션과 상호작용합니다.
예를 들어, 웹 애플리케이션의 경우, 사용자가 웹 페이지를 열고 버튼을 클릭하거나 양식을 작성하는 등의 동작을 시뮬레이션하여 애플리케이션의 응답을 확인합니다.
이러한 기능 테스트의 경우 API툴, 데이터베이스까지 연관되어 있어 HTTP 통신도 해야 되고, 데이터베이스 연결도 해야 되는 등 개발한 애플리케이션과 연관대상이 많기 때문에 단위 테스트로 부르기 힘듭니다.
* 기업에서는 이러한 기능 테스트는 범위가 매우 넓기 때문에 애플리케이션을 개발한 개발자가 할 수도 있지만,
일반적으로 테스트 전문(QA)부서 또는 외부 QA 업체에 의뢰를 합니다.
- 통합 테스트
통합 테스트는 애플리케이션의 다양한 구성요소 또는 서비스들이 함께 작동하는 상황에서 상호작용을 테스트하는 것을
의미합니다. 애플리케이션의 여러 부분이 올바르게 통합되고, 데이터나 정보가 제대로 전달되는지를 확인합니다.
기능 테스트와의 차이점
기능 테스트는 사용자 인터페이스나 API를 통해 애플리케이션의 완전한 흐름을 평가하고,
통합 테스트는 구성 요소들의 상호작용과 데이터 통합을 검증합니다.
예를 들어, 개발자가 Controller의 API를 호출하는 테스트 코드를 작성한 후 실행하면 서비스 계층과 데이터 액세스 계층을 거쳐 DB에 실제로 접속해서 기대했던 동작을 하는지 테스트하는 것은 통합 테스트의 하나라고 볼 수있습니다.
통합테스트 또한 애플리케이션의 여러 계층과 연관되어 있으며, DB까지 연결되어 있어서 독립적인 테스트가 불가능 하기에 단위 테스트라고 하기에는 범위가 큰 편입니다.
- 슬라이스 테스트
슬라이스 테스트는 애플리케이션을 특정 계층으로 쪼개어서 하는 테스트를 의미합니다.
API 계층, 서비스 계층, 데이터 액세스 계층이 슬라이스 테스트의 대상이 될 수 있습니다.
슬라이스 테스트는 전체 애플리케이션을 실행하지 않고, 특정 계층에 초점을 맞추어 테스트를 수행합니다.
하지만 슬라이스 테스트 또한 테스트하려는 계층에서 HTTP 요청이 필요하고, 외부 서비스가 연동되기도 하며 특히나
데이터 액세스 계층의 경우 데이터 베이스와 연동되어 있기 때문에 단위 테스트라고 부르기엔 단위가 큰편입니다.
- 단위 테스트
단위 테스트는 애플리케이션의 가장 작은 단위인 개별적인 코드 단위를 테스트 하는 것입니다.
즉, 함수, 메서드 또는 클래스와 같은 작은 단위의 코드 조각을 테스트하는 것입니다.
일반적으로 개발자가 직접 구현하는 핵심 로직 즉, 비즈니스 로직에서 사용하는 클래스들이 독립적으로 테스트하기 가장 좋은 대상입니다.
비즈니스 로직은 서비스 계층에서 구현합니다.
구현한 기능들이 기대한 대로 빠르게 동작하는지 테스트하기 위해서는 주로 메서드를 테스트합니다.
단위 테스트 코드는 메서드 단위로 대부분 작성된다고 생각하면 될 것 같습니다.
단위 테스트를 해야 되는 이유
단위 테스트를 해야하는 이유는 여러가지가 있습니다.
- 애플리케이션을 만들면서 매번 인텔리제이를 실행하고, 포스트맨을 열어 HTTP요청을 보내는 일을 단순화할 수 있습니다.
- 직접 구현한 코드가 의도한 대로 작동하는지 그 결과를 보다 빠르게 확인할 수 있습니다.
- 작은 단위의 테스트로 미리미리 버그를 찾을 수 있어 애플리케이션의 덩치가 커지기 전에 문제 원인을 찾고 해결할 가능성이 높습니다.
- 테스트 케이스가 잘 짜여져 있으면 버그가 발생하더라도 테스트 케이스를 돌려보면서 문제가 발생한 원인을 단계적으로 찾아가기 용이합니다.
테스트 케이스(Test Case)란?
테스트 케이스란 테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세를 의미합니다.
즉, 메서드 등 하나의 단위를 테스트하기 위해 작성하는 테스트 코드라고 생각하면 되겠습니다.
이 테스트 코드 안에 입력 데이터, 실행 조건, 기대 결과라는 로직들이 포함됩니다.
단위 테스트를 위한 F.I.R.S.T 원칙
단위 테스트를 위한 테스트 케이스를 작성할 때는 FIRST원칙을 참고하여 만들 수 있습니다.
FIRST원칙은 Fast(빠른), Independent(독립적인), Repeatable(반복 가능한), Self-validating(자체 검증 가능한),
Timely(적시에)의 줄임말 입니다.
- Fast (빠른)
단위 테스트는 빠르게 실행되어야 합니다.
빠른 실행 시간은 빠른 피드백을 제공하고, 개발자가 코드를 수정하고 테스트를 반복할 수 있도록 합니다. 느린 테스트는 개발 과정을 느리게 만들고, 생산성을 저하시킬 수 있습니다.
- Independent (독립적인)
각각의 단위 테스트는 독립적으로 실행되어야 합니다.
다른 테스트에 의존하지 않고, 어떤 순서로 실행되더라도 동일한 결과를 보장해야 합니다.
예를 들어, A라는 테스트 케이스를 먼저 실행시킨 후에 다음으로 B라는 테스트 케이스를 실행시켰더니 테스트에 실패하게 된다면 테스트 케이스끼리 독립적이지 않은 것입니다.
독립적인 테스트는 테스트의 신뢰성을 높이고, 디버깅과 유지 보수를 용이하게 만들어줍니다.
- Repeatable (반복 가능한)
단위 테스트는 언제나 반복 가능해야 합니다.
동일한 입력 값을 주면 항상 동일한 결과가 나와야 합니다.
반복 가능한 테스트는 코드의 변경이나 환경의 변화에 영향을 받지 않고 동작함을 보장하며, 일관된 결과를 얻을 수 있습니다.
외부 서비스나 외부 리소스가 연동되는 경우 앞에서 언급한 원칙들을 포함해서 동일한 테스트 결과 역시 보장하지 못하기 때문에 단위 테스트 시에는 외부의 서비스나 리소스의 연동을 끊어주는 것이 바람직합니다.
- Self_validating (자체 검증 가능한)
단위 테스트는 자체적으로 검증 가능해야 합니다.
테스트 코드 자체가 예상 동작을 검증하고, 예상 출력과 실제 출력을 비교하여 테스트의 판정을 내릴 수 있어야 합니다.
이말은 테스트 케이스 스스로 결과가 옳은지 그른지 판단할 수 있어야 한다는 것입니다.
- Timely (적시에)
단위 테스트는 코드를 작성하기 전에 작성되어야 합니다.
즉, 개발 과정의 일부로 테스트가 계획되고 작성되어야 합니다.
테스트가 코드 작성 후에 뒤늦게 작성되면 버그 발견과 수정에 대한 비용이 크게 증가할 수 있습니다.
FIRST 원칙은 단위 테스트를 효과적으로 작성하고 유지하기 위한 원칙으로, 빠르고 독립적이며 반복 가능하며 자체 검증 가능하며 적시에 작성되어야 함을 강조합니다.
이를 따르면 신뢰성 높은 테스트를 작성할 수 있고, 코드의 변경에 따른 부작용을 신속하게 파악하여 개발 생산성을 향상시킬 수 있습니다.
JUnit 없이 비즈니스 로직에 단위 테스트 적용해 보기
Junit없이 기본적으로 테스트 케이스를 작성하는 흐름을 보면서 테스트 케이스를 작성하는 감을 잡아 보도록 하겠습니다.
public class StampCalculator {
public static int calculateStampCount(int nowCount, int earned) {
return nowCount + earned;
}
}
먼저 커피 주문 샘플 애플리케이션에서 회원이 보유한 스탬프 수와 회원이 주문한 커피수량 만큼 획득한 스탬프 수를 더해서 누적 스탬프 수를 계산해 주는 헬퍼 클래스를 만들어 보겠습니다.
단위 테스트 학습을 목적으로 단 하나의 메서드만 가지고 있는 이 StampCalculator의 calculateStampCount() 메서드가 기대했던 대로 잘 동작하는지 검증하는 테스트 케이스를 작성해 보도록 합시다.
package com.codestates.helper;
public class StampCalculatorTestWithoutJUnit {
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);
}
}
위 예시 코드는 JUnit을 사용하지 않고, StampCalculator의 calculateStampCount()를 테스트 하는 테스트 케이스입니다.
테스트 대상은 StampCalculator.calculateStampCount() 메서드이고, 현재 주어진 스탬프 수가 5(nowCount)이고, 주문으로 얻게 되는 스탬프 수가 3(earned)인데 기대하는 값은 7(expected)이라고 예상하고 있습니다.
코드를 실행하면 당연히 계산 결과는 8(actual)이기 때문에 결과 값인 expected == actual은 false입니다.
즉, expected로 틀린 값인 7을 기대했기 때문에 결과(actual) 값으로 false가 나왔다는 것은 테스트에 실패한 것입니다.
위 예시 코드에서 주석으로 표시한 given - when - then이라는 용어는 BDD(Behavior Driven Development)라는 테스트 방식에서 사용하는 용어입니다.
단위 테스트에 익숙하지 않은 분들에게 또는 테스트 케이스의 가독성을 높이기 위해 given - when - then 표현 방법을
사용하는 것은 테스트 케이스를 작성하는데 유용한 방법입니다.
- Given
- 테스트를 위한 준비 과정을 명시할 수 있습니다.
- 테스트에 필요한 전제 조건들이 포함된다고 보면 됩니다.
- 테스트 대상에 전달되는 입력 값(테스트 데이터) 역시 Given에 포함됩니다.
- When
- 테스트할 동작(대상)을 지정합니다.
- 단위 테스트에서는 일반적으로 메서드 호출을 통해 테스트를 진행하므로 한두 줄 정도로 작성이 끝나는 부분입니다.
- Then
- 테스트의 결과를 검증하는 영역입니다.
- 일반적으로 예상하는 값(expected)과 테스트 대상 메서드의 동작 수행 결과(actual) 값을 비교해서 기대한 대로 동작을 수행하는지 검증(Assertion)하는 코드들이 포함됩니다.
Assertion(어써션)이란?
테스트 세계에서 Assertion(어써션)이라는 용어는 테스트 결과를 검증할 때 주로 사용합니다.
테스트 케이스의 결과가 반드시 참(true)이어야 한다는 것을 논리적으로 표현한 것이 Assertion(어써션)인데,
한마디로 ‘예상하는 결과 값이 참(true)이길 바라는 것’이라고 이해하면 될 것 같습니다.
Assertion(어써션)을 단언문, 단정문이라고 표현을 하는 곳이 많은데 우리는 앞으로 Assertion을 부를 때,
이름 그대로 Assertion(어써션)이라고 부르도록 하겠습니다.
이번엔 다른 기능을 하나더 추가하겠습니다.
public class StampCalculator {
// (1)
public static int calculateStampCount(int nowCount, int earned) {
return nowCount + earned;
}
// (2)
public static int calculateEarnedStampCount(Order order) {
return order.getOrderCoffees().stream()
.map(orderCoffee -> orderCoffee.getQuantity())
.mapToInt(quantity -> quantity)
.sum();
}
}
위 코드 예시를 보면 테스트 대상 클래스에 기능이 하나 더 추가되었습니다.
(2)의 calculateEarnedStampCount() 메서드는 회원이 주문한 주문 정보에서 얻게되는 스탬프 개수를 계산하는 기능을 합니다.
Given-When-Then으로 테스트 케이스를 설명하면 다음과 같습니다.
- given
- 주문한 커피의 수량이 필요하기 때문에 Order와 OrderCoffee 객체를 직접 만들어서 테스트에 필요한 데이터를 생성합니다.
- when
- 테스트 대상인 StampCalculator.calculateEarnedStampCount()에 given에서 생성한 테스트 데이터를 입력값으로 전달합니다.
- 이번 테스트 케이스의 목적은 바로 StampCalculator.calculateEarnedStampCount() 메서드가 잘 동작하는지를 확인하는 것입니다.
- then
- 주문한 커피 수량만큼의 스탬프가 계산되는지를 Assertion합니다.
given에서 데이터를 수동으로 넣어주는 이유는 테스트 케이스의 목적이 StampCalculator.calculateEarnedStampCount()
메서드의 동작을 테스트하는 것이기 때문입니다.
StampCalculator.calculateEarnedStampCount() 메서드의 파라미터로 주어지는 입력 값인 Order(주문) 객체를 통해 얻게 되는 스탬프 개수를 잘 계산하는지 Assertion하는 것이 핵심이기 때문에 테스트 데이터가 입력 값으로 필요합니다.
그 입력값이 바로 given에서 사용한 OrderCoffee 객체를 포함한 Order 객체인 것입니다.
위 코드 예시를 실행하면 두 개의 테스트 케이스가 실행되고 두 개의 결과가 다음과 같이 콘솔에 출력됩니다.
false
true
두 개의 테스트 케이스를 한꺼번에 실행하고 순서를 바꿔서 사용하더라도 각각의 테스트 케이스는 독립적으로 실행되기 때문에
테스트 케이스를 실행할 때마다 테스트 결과가 바뀌는 경우는 없습니다.
즉, 간단한 예제 코드이지만 앞에서 설명한 F.I.R.S.T 원칙을 그럭저럭 잘 따른다고 볼 수 있습니다.
'코드 스테이츠' 카테고리의 다른 글
세션 기반 자격 증명 방식 / 토큰 기반 자격 증명 방식 (0) | 2023.07.13 |
---|---|
Spring Security - 보안 (0) | 2023.07.10 |
트랜잭션(Transaction) (0) | 2023.06.26 |
페이지네이션(Pagination) (0) | 2023.06.20 |
DDD(Domain Driven Design),애그리거트(Aggregate) (0) | 2023.06.19 |