테스트(Test)
소프트웨어 공학에서 말하는 테스트(Test)는 소프트웨어의 품질 기능을 확인하고 버그를 찾아내는 과정을 말합니다. 시스템의 안정성과 성능 등 다양한 측면을 평가 하고 문제점을 찾기 위해 반드시 필요한 과정입니다.
테스트 방식은 아래 두 가지로 나뉘게됩니다.
- 수동 테스트
- 자동 테스트
수동 테스트
수동 테스트는 소프트웨어를 직접 실행해 보면서 각 기능이 요구사항에 맞게 동작하는지 확인하는 검증 과정입니다.
자동화 도구 대신 사람이 직접 화면을 조작하며 테스트하기 때문에, 실제 사용자 관점에서 기능의 흐름과 동작을 점검할 수 있다는 특징이 있습니다.
자동 테스트
자동 테스트는 테스트 스크립트나 도구를 활용해 소프트웨어를 자동으로 검증하는 방식입니다. 사람이 직접 반복 수행하지 않아도 되기 때문에 동일한 테스트를 빠르고 일관성 있게 실행할 수 있으며, 회귀 테스트나 대규모 기능 검증에 특히 효과적입니다. 코드 변경이 발생하더라도 자동으로 테스트를 실행해 안정성을 확인할 수 있어, 휴먼 에러를 줄이고 개발 생산성을 높이는 데 큰 도움이되는 방법입니다.
테스트 피라미드 (Test Pyramid)

테스트는 목적과 범위, 그리고 검증 대상에 따라 여러 종류로 나뉩니다. 그중 가장 널리 사용되는 분류 방식이 바로 테스트 피라미드(Test Pyramid) 구조입니다. 테스트 피라미드는 테스트를 계층 형태로 구성한 모델로, 아래에서 위로 올라갈수록 실제 사용자 환경과 더 가까운 테스트를 의미합니다. 아래 계층일수록 빠르고 가볍게 많이 실행할 수 있고, 위 계층일수록 실행 비용은 크지만 실제 동작을 종합적으로 검증할 수 있다는 특징이 있습니다. 즉, 각 테스트는 서로 대체 관계가 아니라 역할이 다른 보완 관계라고 볼 수 있습니다. 아래에서는 각 계층이 어떤 역할을 하는지 하나씩 살펴보겠습니다.
단위 테스트(unit test)
단위 테스트는 소프트웨어를 구성하는 가장 작은 단위(Unit)를 검증하는 테스트입니다. 여기서 Unit은 함수, 메서드, 클래스처럼 독립적으로 동작하는 작은 코드 조각을 의미하며, 각 객체나 컴포넌트가 맡은 책임을 정확히 수행하는지 확인하는 데 목적이 있습니다. 즉, 작은 기능 하나가 의도한 대로 정확하게 동작하는지 빠르게 검증하는 가장 기본적인 테스트 단계라고 볼 수 있습니다.
통합 테스트(integration test)
통합 테스트는 여러 컴포넌트나 객체가 함께 동작하는 상황을 검증하는 테스트입니다. 각 단위를 독립적으로 확인하는 단위 테스트와 달리, 실제로 이들이 연결되어 상호작용할 때 발생할 수 있는 문제를 점검하는 데 목적이 있습니다.
특히 외부 시스템 연동, 데이터베이스 처리, 예외 상황 등 다양한 환경에서 시스템이 올바르게 동작하는지 확인하며, 전체 흐름 속에서의 안정성을 검증하는 단계라고 볼 수 있습니다.
E2E 테스트(end-to-end test)
E2E 테스트(End-to-End 테스트)는 실제 사용자 시나리오를 기반으로 시스템 전체가 어떻게 동작하는지 검증하는 테스트입니다. 로그인부터 주요 기능 실행, 결과 확인까지 하나의 흐름을 처음부터 끝까지 실행하며, 서비스가 실제 환경에서 정상적으로 동작하는지를 확인합니다. 테스트 피라미드의 최상단에 위치하는 만큼 단위 테스트나 통합 테스트보다 실행 비용은 크지만, 사용자와 가장 가까운 환경에서 전체 기능을 종합적으로 검증할 수 있다는 특징이 있습니다.
테스트 작성하기
테스트 코드를 작성하기에 앞서, 먼저 Given-When-Then 패턴에 대해 간단히 정리해보겠습니다. Behavior Driven Development(BDD)에서 파생된 테스트 작성 방식으로, 테스트를 준비 → 실행 → 검증 단계로 나누어 구조적으로 표현하는 패턴입니다. 각 단계의 역할이 명확해 테스트 의도를 쉽게 이해할 수 있고, 가독성 높은 테스트 코드를 작성하는 데 도움이 됩니다.
Given-When-Then
- Given : 테스트에 필요한 환경을 준비하는 단계
(객체 생성, 변수 설정, Mock 정의 등) - When : 실제 테스트 동작을 수행하는 단계
(메서드 호출, 로직 실행 등) - Then : 실행 결과를 검증하는 단계
(기대값과 결과 비교, 예외 발생 여부 확인 등)
이 패턴을 사용하면 테스트의 흐름이 자연스럽게 읽혀, 코드의 목적을 한눈에 파악할 수 있다는 장점이 있습니다.
주요 어노테이션
| 어노테이션 | 설명 |
| @Test | 테스트 메서드임을 선언하며, 해당 메서드를 테스트로 실행 |
| @BeforeEach | 각 테스트 실행 전마다 호출되는 초기화 메서드 |
| @AfterEach | 각 테스트 실행 후마다 호출되는 정리 메서드 |
| @BeforeAll | 전체 테스트 시작 한 번만 실행되는 공통 초기화 메서드 |
| @AfterAll | 전체 테스트 종료 한 번만 실행되는 공통 정리 메서드 |
| @DisplayName | 테스트 이름을 사람이 읽기 쉬운 문자열로 지정 |
| @Disabled | 테스트를 일시적으로 실행 대상에서 제외 (스킵) |
예시 코드
class UserServiceTest {
// 전체 테스트 시작 전 1회 실행
@BeforeAll
static void beforeAll() {
System.out.println("테스트 시작!");
}
// 각 테스트 실행 전마다 실행
@BeforeEach
void setUp() {
System.out.println("테스트 환경 초기화");
}
@Test
@DisplayName("회원 생성이 정상적으로 동작합니다")
void createUser() {
System.out.println("회원 생성 테스트 실행");
}
@Test
@Disabled("아직 구현 중인 기능")
void deleteUser() {
System.out.println("실행되지 않았습니다");
}
// 각 테스트 실행 후마다 실행
@AfterEach
void tearDown() {
System.out.println("테스트 데이터 정리");
}
// 전체 테스트 종료 후 1회 실행
@AfterAll
static void afterAll() {
System.out.println("테스트 종료!");
}
}
테스트가 실행되면 전체 흐름은 다음과 같이 진행됩니다. 먼저 @BeforeAll이 가장 처음 한 번만 실행되어 테스트 전체에서 공통으로 사용할 환경을 초기화합니다. 그 이후 각 테스트가 시작될 때마다 @BeforeEach가 호출되어 테스트에 필요한 객체 생성이나 데이터 세팅을 수행합니다. 환경 준비가 끝나면 @Test 메서드가 실행되어 실제 테스트 로직이 동작합니다. 테스트가 종료되면 @AfterEach가 호출되어 사용한 자원이나 데이터를 정리합니다. 이 과정이 테스트 개수만큼 반복되며, 모든 테스트가 끝난 뒤 마지막으로 @AfterAll이 한 번만 실행되어 전체 테스트 마무리 작업을 수행합니다. 참고로 @Disabled가 붙은 테스트는 실행 과정에서 제외되며, @DisplayName은 실행 시 사람이 읽기 쉬운 이름으로 표시되어 테스트 의도를 더 명확하게 보여줍니다.
단언문(Assertions)
| 메서드 | 설명 |
| assertEquals(expected, actual) | 기대값과 실제 값이 같은지 검증 |
| assertTrue(condition) | 조건이 true인지 검증 |
| assertFalse(condition) | 조건이 false인지 검증 |
| assertThrows(Exception.class, () -> 실행코드) | 특정 예외가 발생하는지 검증 |
| assertAll() | 여러 검증을 묶어 한 번에 실행 (모든 단언 수행) |
| assertNotNull(object) | 객체가 null이 아닌지 검증 |
예시 코드
class CalculatorTest {
@Test
void assertionExample() {
int result = 2 + 3;
// 값 비교
assertEquals(5, result);
// 조건 검증
assertTrue(result > 0);
assertFalse(result < 0);
// null 체크
String name = "HONG";
assertNotNull(name);
// 예외 검증
assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("잘못된 값 입니다.");
});
// 여러 검증을 묶어서 실행
assertAll(
() -> assertEquals(5, result),
() -> assertTrue(result == 5),
() -> assertNotNull(name)
);
}
}
단언문(Assertion)은 테스트의 결과가 우리가 기대한 대로 나왔는지 확인하는 역할을 합니다. 테스트 코드에서 로직을 실행한 뒤, assert 계열 메서드를 사용해 “이 값이 맞는지”, “정상적으로 동작했는지”를 검증하는 단계라고 보면 됩니다. 가장 기본이 되는 assertEquals는 기대값과 실제 값을 비교할 때 사용하며, 단순한 결과 검증에 가장 많이 활용됩니다. assertTrue와 assertFalse는 조건식이 참인지 거짓인지 확인할 때 사용해, 특정 로직의 상태를 체크하기에 좋습니다. 또한 assertNotNull은 객체가 정상적으로 생성되었는지 확인할 때 유용합니다. 예상하지 못한 NullPointerException을 방지하는 데 도움이 되기 때문에 실무에서도 자주 사용됩니다. 예외 상황을 검증할 때는 assertThrows를 사용합니다. 특정 코드 실행 시 지정한 예외가 발생해야 테스트가 통과하는 방식으로, 말 그대로 “실패하는 상황까지 테스트하는 것” 이라고 이해하면 쉽습니다. 마지막으로 assertAll은 여러 검증을 한 번에 묶어 실행할 수 있게 해줍니다. 중간에 하나가 실패하더라도 나머지 검증까지 모두 수행한 뒤 결과를 보여주기 때문에, 여러 조건을 함께 확인해야 할 때 특히 유용합니다. 결국 단언문은 테스트의 마지막 단계에서 “그래서, 제대로 동작했는가?” 를 판단해주는 기준점이라고 생각하면 됩니다.
기술과 개발의 차이(feat: 회고)
사실 이번에 테스트를 정리하면서, 한 가지 더 꼭 이야기해보고 싶은 주제가 떠올랐습니다. 바로 제가 책을 읽으며 인상 깊게 느꼈던 ‘기술과 개발의 차이’ 에 대한 내용입니다. 우리는 흔히 개발 공부를 한다고 하면 프로그래밍 언어, 인프라, 프레임워크, 특정 라이브러리 같은 기술 스택을 먼저 떠올립니다. 하지만 곰곰이 생각해보면, 이런 것들은 ‘개발 능력’ 이라기보다는 ‘도구를 하나 더 다룰 수 있게 되는 것’ 에 가깝습니다. 예를 들어 Spring Boot를 공부했다고 해서 개발 실력이 갑자기 좋아지는 것은 아닙니다. 단지 Spring이라는 기술을 사용할 수 있게 된 것뿐이지, 문제를 해결하는 능력이나 좋은 구조를 설계하는 힘이 바로 생기는 것은 아니기 때문입니다. 책에서도 이 부분을 강조하며, 진짜 개발 역량을 키워주는 요소로 몇 가지를 소개했는데, 그중 하나가 바로 테스트였습니다. 이 대목이 특히 인상 깊었고, 그래서 이번 글에도 테스트에 대한 내용을 함께 담아보았습니다. 테스트는 분명 번거롭습니다. 코드를 한 번 더 작성해야 하고, 검증하고, 실패 케이스까지 신경 써야 하니 당장은 귀찮게 느껴질 수 있습니다. 하지만 테스트는 코드의 안정성을 보장하고, 변경에 대한 두려움을 줄여주며, 더 나은 설계를 가능하게 만드는 가장 강력한 도구입니다. 결국 테스트는 ‘기술’이 아니라, 좋은 개발자가 되기 위한 필수 습관이라는 것을 이번 과정을 통해 다시 한번 느끼게 되었습니다.
'TIL & 트러블 슈팅' 카테고리의 다른 글
| [내일배움캠프] - DAY20 IP와 네트워크 통신에 대해 알아보자 (0) | 2026.03.10 |
|---|---|
| [내일배움캠프] - DAY19 AOP(Aspect Oriented Programming)에 대해 알아보자 (0) | 2026.03.06 |
| [내일배움캠프] - DAY17 코드 개선 과제 (1) | 2026.03.04 |
| [내일배움캠프] - DAY16 일정관리 앱 만들기-2 (1) | 2026.02.12 |
| [내일배움캠프] - DAY15 일정관리 앱 만들기 (0) | 2026.02.04 |
