📣 테스트 코드 [ 이론 ]

예시 1. 댓글 삭제를 테스트 하려할때
회원가입 , 로그인 api 거친후 댓글 등록후 삭제 까지 많은 API를 거쳐야 하기 때문에 회귀 테스트 대상이 많다.
1️⃣ 왜 테스트를 해야 할까?
현대 개발 직군에서 테스트는 필수적인 절차, 코드가 단 한 번에 완벽히 구현되기는 어렵고, 다양한 예외 상황과 복잡한 요구사항이 존재하기 때문이다.
- 예측 불가능한 사용자 행동: 실제 사용자들은 우리가 예상하지 못한 입력이나 상황을 유발할 수 있음. ➡ 미리 코드를 짜놓고 수행
- 복잡한 비즈니스 로직: 조건 분기가 많은 로직은 예상치 못한 오류를 일으킬 수 있음.
- 변경과 확장: 새로운 기능 추가나 리팩토링 과정에서 기존 기능이 망가질 수 있으며, 이를 사전에 방지하려면 안정적인 테스트가 필요 ( 배포하기전에 알아 챌수 있다 ✅✅ )
테스트를 통해 조기에 버그를 발견하고, 신뢰성을 확보할 수 있으며, 이는 곧 제품 품질 향상과 유지보수 비용 절감으로 이어집니다.
2️⃣ 테스트 코드의 이점
테스트 코드 작성은 추가적인 비용처럼 볼 수 있지만, 장기적으로 개발 효율성과 안정성을 극대화 함.
- 장애 예방 ⭐
- 테스트 코드는 코드 변경 및 확장시 발생할 수 있는 회귀 오류를 방지 (이전에 해나갈려고 했던 방식 = 회귀 오류)
- 변경 사항이 다른 기능에 미치는 영향을 빠르게 확인할 수 있음
- 유지보수 비용 절감 [ 개발자의 인건비, 신뢰, 시간 ]
- 초기 단계에서 버그 발견시 사후 수정 비용보다 저렴하다.
- 테스트 코드가 없는 경우 복잡한 코드의 로직을 추적하고 수정하는데 큰 리소스를 소모.
- 효율적인 협업 도구
- 테스트 코드는 팀 내 개발자 간의 자식 공유 도구로 사용됨.
- 코드 리뷰의 신규 개발자 온보딩 과정에서 테스트 코드가 해당 기능의 동작 방식을 명확히 설명.
- 배포 안정성 확보
- 테스트 코드를 통해 배포 전에 애플리케이션의 안정성을 철저히 검증함.
- 특히, 자동화된 회귀 테스트는 배포 직전에도 전체 시스템의 품질을 보장.
사례

3️⃣ 테스트 코드 작성 / 미작성 차이
| 항목 | 테스트 코드 작성 | 테스트 코드 미작성 |
| 초기 개발 비용 | 개발 시간과 비율 증가 | 빠르게 개발 가능 |
| 버그 수정 비용 | 초기에 발견된 버그를 빠르고 저렴히 해결 가능 | 배포 후 발견 시 높은 비용 발생 |
| 품질 | 코드 변경 시 안정성을 보장 | 코드 변경 시 회귀 오류 발생 가능성 ⬆ |
| 유지보수 | 기능 문서화 및 코드 이해도 ⬆ | 신규 팀원이 코드 이해 및 적응하는데 시간 소요 |
| 배포 안정성 | 자동화 테스트로 배포 전 품질 보장 | 배포 후 오류 발견 시 롤백 및 긴급 수정 필요 |
| 장애 발생 확률 | 회귀 테스트를 통해 장애 가능성 최소화 | 장애 발생 가능성 증가 |
💥 대기업과 스타트업의 테스트코드를 바라보는 시각
| 항목 | 대기업 | 스타트업 ex) ChatGPT |
| 우선순위 | 품질, 안정성, 신뢰성 보장 | 빠른 개발, 시장 출시 속도 |
| 리소스 | 충분한 인력과 예산, QA팀 | 제한된 리소스, 소수의 개발자가 전담 |
| 테스트 커버리지 | 높은 커버리지 목표 ( 80 % 이상 ) | 핵심 기능 중심의 최소한의 테스트 |
| 자동화 | CI/CD 파이프라인에 통합된 자동화 테스트 | 자동화 테스트가 부족 or 수동 테스트 위주 |
| 관점 | 장기적인 유지보수와 확장성 | 단기적인 시장 검증과 유연성 |
대기업은 안정성과 품질 보증에 초점, 프로세스를 표준화하여 테스트 코드 작성에 많은 리소스를 투자.
스타트업은 속도와 유연성을 중시, 필요시 실용적으로 테스트 코드를 작성하는 경향
4️⃣ S/W 테스트의 7원칙
- 테스트는 결함이 존재함을 밝히는 활동이다.
- S/W에 대해 테스트 완료 및 발견된 이슈를 모두 해결해도 결함이 없다는 것을 증명할 수 있는것은 아니다. 또한 이슈가 발견되지 않다고 해서 결하밍 없다는 것이 증명 되지는 않는다. 테스트느는 프로그램의 결함이 없음을 보장하는 활동이 아니라, 결함이 존재함을 밝히기 위한 활동이다.
- 완벽한 테스팅은 불가능하다
- 매우 단순한 S/W가 아닌 이상 내부 조건, 입력값, 타이밍에 대한 모든 조합을 확인할 수 없다. 따라서 테스트 대상의 리스크 분석 후에 가장 중요한 부분을 중점으로 테스팅 리소르를 투입하여야 한다.
- ⭐ 테스팅은 개발 초기 단계에서 시작해야 한다.
- 요구사항 분석 및 설계 단계에서부터 테스트를 진행하는 경우 문서상의 결함을 확인할 수 있다. 이러한 결함은 코딩 작업 이후에 발견되는 결함에 비해 훨씬 간단히 해결할 수 있다. 또한 조기에 테스트 설계를 마친 경우 코딩이 완료되자마자 테스트 케이스를 레벨별로 실행할 수 있어 전체 테스트 기간을 단축할 수 있다.
- 결함 집중
- 대다수의 결함은 소수의 특정 모듈에 집중되는 경향, 이러한 결함은 장애를 발생시킬 가능성이 높음.
- 자체적으로 복잡한 구조를 가진 모듈
- S/W의 다른 부분과 복잡한 상호 작용하는 모듈
- 개발 난이도가 높거나 최신 기술을 사용한 모듈
- 기존의 것을 사용하지 않고 새롭게 개발한 모듈
- 크기가 큰 모듈
- 경험이 미흡한 개발팀에서 개발한 모듈
- 대다수의 결함은 소수의 특정 모듈에 집중되는 경향, 이러한 결함은 장애를 발생시킬 가능성이 높음.
- 살충제 패러독스
- 동일한 테스트 케이스를 반복적으로 수행하는 경우 더 이상 새로운 결함을 찾아낼 수 없다. 이를 극복하려면 새로운 기법, 다른 시각에서 테스트 케이스를 정기적으로 변경하고 추가해주어야 한다.
- 테스팅은 정황에 따라 이루어저야 한다.
- S/W의 종류나 목표 등에 따라 해당 S/W에 맞는 테스트 방식이 적용되어야 한다. 개발 프로젝트인지 운영중인 시스템의 유지보수인지 여부, 사용 가능한 예산, 출시 일정, 리스크, 조직 문화, 사용자의 기대, 테스팅이 필요한 기반 환경의 가용성, 프로젝트의 중요도 등 고려되어야 한다.
- 오류 부재의 궤변
- 거의 모든 결함을 확인 후 제거한다 해도 사용자의 요구 또는 비즈니스 목적을 충족하지 못하는 경우 품질이 높다고 할수 없다.
5️⃣ FIRST 원칙
이 원칙을 지키면 테스트 코드가 유지보수하기 쉬워지고, 개발 생산성이 증가
- Fast ( 빠르게 ) : 테스트는 짦은 시간 안에 실행되어야 함.
- Independent ( 독립적 ) : 각 테스트는 다른 테스트에 영향을 받지 않고, 독립적으로 수행 가능해야 함.
- Repeatable( 반복 가능 ) : 언제나 동일한 결과를 얻을수 있어야 함.
- Self-checking( 자기 검증 ) : 테스트 결과가 성공/실패를 명확히 나타내어 사람이 추가로 판별할 필요없어야 함.
- Timely( 적시에 작성 ) :실제 코드를 구현하기 전 or 최소한 함께 작성해 가치 있는 피드백을 얻어야 함.
6️⃣ 테스트 피라미드

아래로 갈수록 테스트가 많고, 비용이 적음, 자동화 난이도 낮다.
위로 갈수록 테스트가 적고, 비용이 많음, 자동화 난이도 높음.
7️⃣ 단위 테스트(Unit Test)와 통합 테스트 (Integration Test)
- 단위 테스트(Unit Test)
- 특징: 작은 단위(클래스, 메서드, 함수)의 로직을 독립적으로 검증
- 장점: 실행 속도가 빠르고, 문제 발생 지점을 정확히 파악 가능
- 단점: 연동 되었을 때, 발생하는 문제점을 파악하기 어렵다.
- 예시: 비즈니스 로직 함수 하나를 외부 의존성 없이 검증하기
- 통합 테스트(Integration Test)
- 특징: 여러 모듈이 함께 작동하는 과정에서 문제가 없는지 확인
- 장점: 실제 환경과 유사하게 동작하므로 배포 전 전체 흐름 검증 가능
- 단점: 환경설정 하기가 힘들다, 오류가 발생해도 추적하기 힘들다, 시간이 오래걸림, 비용이 많이듬
- 예시: DB와 연동, API 서버 호출, 여러 계층(Controller-Service-Repository) 조합 검증
일반적으로 단위 테스트를 통해 코드 품질을 관리, 통합 테스트를 통해 전체 시스템의 안정성을 확보하는 전략을 사용
단위 테스트는 빠르고 자주 실행할 수 있으며, 통합 테스트는 배포 전 중요한 시점에 실행해 시스템 전체를 검증하는 식으로 활용됨
8️⃣ BDD와 TDD의 등장
TDD(Test Driven Development)와 BDD(Behavior Driven Development)는 테스트가 개발 과정의 핵심 요소로 자리 잡게 만든 철학적인 방법론. 테스트 코드를 잘 활용하는 개발 패러다임.
- TDD ( 테스트 주도 개발 )
- 등장 배경
- 개발을 진행 하다 보면 코드를 끝까지 작성한 후에야 "정말 작동하는지"를 확인하는 테스트를 진행
- 계속 반복하다보면 오류를 수정하고나서 내가 뭘 만들어야 하는지 잊어버리고 일단 만들다 설계에서 벗어나는 일이 비일비재 함.
- 위와 같은 상황을 해결하기위해 먼저 설계 후 동작하는 코드를 작성하는 패러다임 TDD가 나옴.
- 개념 : Test(실패하는 테스트 작성) ➡ Code (테스트 통과를 위한 최소한의 코드) ➡ Refactor(코드 개선)
- 효과 : 기능 구현 이전에 테스트를 작성함으로써, 구현 대상과 범위를 명확하게 정의하고, 불필요한 로직을 만들지 않게 도와줌.
- 등장 배경
- BDD ( 행위 주도 개발 )
- 등장 배경
- TDD는 개발자들끼리는 의사소통이 잘 되지만 비 개발자에게는 많이 어렵다.
- 또한 협업을 진행한다면 비 개발자 인원이 더 많으며, 프로젝트를 이끌어가는 기획자 의견은 굉장히 중요함.
- 테스트를 사람이 읽을 수 있는 언어로 작성하기 시작해 개발자와 비 개발자가 협업이 원활해짐
- 개념 : 사용자(또는 비즈니스)의 행위에 초점을 맞춰 Given-When-Then 형태로 테스트 시나리오 작성
- 효과 : 비즈니스 팀, 기획자, 개발자 모두가 이해하기 쉬운 시나리오로 테스틀 기술하므로, 커뮤니케이션 비용 감소 및 요구사항 명확화.
- 등장 배경
| 특징 | TDD | BDD |
| 초점 | 코드를 올바르게 작성하기 위한 기술적 기반 | 비즈니스 요구사항을 기반으로 시스템 동작 정의 |
| 언어 | 개발자 중심, 기술적인 테스트 코드 | 비 개발자도 이해할 수 있는 행동 기반 서술 |
| 목표 | "내 코드가 제대로 작동하나?" | "사용자와 비즈니스 요구를 충족시키는가?" |
| 예시 | assertEquals(add(2,3), 5) | given two value when insert function then add two value 유저가 준 두개의 값을 입력해 주면 더하는 기능을 만들어주세요 |
9️⃣ Spring Boot 환경에서의 테스트 방법
- Spring Boot Starter Test : spring-boot-starter-test 의존성 추가만으로, Junit, Mockito, AssertJ등 주요 테스트 라이브러리가 자동으로 포함됨
- @SpringBootTest
- 실제 애플리케이션 컨텍스트를 로드해 통합 테스트 진행
- 모든 빈이 등록되며, 웹 환경 설정 여부도 옵션으로 지정가능
- @WebMvcTest
- Controller 계층만 로드해 웹 계층 테스트에 집중
- MockMvc 활용으로 실제 서버 없이 HTTP 요청/응답 테스트 가능
- @ExtendsWith
- @ExtendWith는 Junit 5에서 사용자 정의 확장을 적용할 때 사용됨. Spring과 관련이 없으며 다양한 테스트 환경을 확장 할수 있음.
- @DataJpaTest
- JPA 관련 빈만 로딩하여 DB 연동 테스트에 특화
- 인메모리 DB(H2)와 함께 사용 시 실제 DB 없이도 Repository 검증가능
✅ Mocking 이란?
Mocking이라는 개념은 테스트 코드를 작성할 때, 실제 객체나 외부 의존성을 흉내 내는(Mock) 가짜 객체를 만들어 사용하는 것을 의미, 쉽게 말해 "실제 서비스를 대체할 수 있는 가짜 서비스"를 만들고, 그걸 이용해 테스트를 진행하는 기법임.
왜 Mocking이 필요한가?
- 외부 자원에 의존적인 로직 테스트를 간편하게 만들기 위해
- 예를 들어, Spring 애플리케이션에서 서비스를 테스트하려고 할 때 보통 DB나 외부 API를 호출하는 경우가 많음.
- 만약 실제 DB나 외부 API를 매번 연결해야 한다면, 테스트 환경 세팅이 번거롭고, 네트워크 문제나 DB 셋업 문제 등으로 인해 테스트가 실패하거나 느려질 수 있음.
- 이런 상황에서 가짜로 DB를 흉내 내는 Mock 객체를 사용하면, DB 연걸 없이 "DB가 있다고 가정" 하고 테스트를 진행할 수 있음.
- 독립적이고 빠른 테스트
- 테스트 코드는 작은단위( 단위테스트, 유닛테스트)로 빠르게 돌아야 의미가 있음.
통합테스트 제외 - 하지만 모든 의존 객체를 실제로 연결하면 속도가 느려지고, 다른 요소들( 네트워크나 DB 상태 )에 의존적이 되어 테스트 결곽가 일관적이지 않게 된다.
- Mocking을 통해 서비의 핵심 로직을 독립적으로 검증할 수 있고, 테스트 속도도 매우 빨라져 신뢰도를 높인다.
- 테스트 코드는 작은단위( 단위테스트, 유닛테스트)로 빠르게 돌아야 의미가 있음.
- 복잡한 로직은 단순화하고 에러를 일찍 발견
- 테스트 할때 외부 로직을 단순화함으로써, 내 코드가 의도대로 동작하는지 빠릴 확인할 수 있습니다.
- 에러가 발생하면 문제를 다른 곳(예 : DB 설정 오류)에서 찾기보다, 바로 해당 메소드의 로직에서 검증할 수 있다.
Test에서 Mocking이 중요한 이유
- 단위 테스트(Unit Test)의 독립성 보장
- 실제 DB, 네트워크, 파일 시스템 등 외부 의존성과 분리함으로써 테스트 실행 속도를 빠르게 하고,테스트 결과를 안정적으로 재현 할수 있다.
- ex) 네트워크 상태나 DB 연결에 의존하지 않고도 테스트가 항상 동일한 결과를 내도록 함.
- 테스트 범위(Coverage)와 정확성 향상
- 원하는 시나리오(성공, 예외 케이스)를 구체적으로 설정하고, 해당 시나리오에 대한 응답을 Mock객체에서 제어할 수 있어 다양한 케이스를 테스트하기 용이함.
- ex) "사용자 조회 시 DB가 에러를 반환"이라는 케이스를 실제 DB 세팅 없이 Mock으로 시뮬레이션 가능.
- 테스트 작성 및 유지보수 용이
- Mock을 사용해 복잡한 의존성을 간단하게 대체하면 테스트 코드가 가벼워지고 직관적
- 서비스 로직 변경 시에도, 실제 인프라 설정을 변경할 필요 없이 Mock 응답만 수정하면 되므로 유지 보수가 수월
- 빠른 피드백 루프
- TDD나 BDD 시나리오에서 Mock을 사용하면, 애플리케이션 전체를 구동하지 않고도 특정 계층 (Controller, Service)만 빠르게 테스트 해볼 수 있음.
- 빌드 파이프라인에서도 단위 테스트는 가장 먼저 수행되며, 빠르게 실패/성공 여부를 알려줌
- 결합도 낮추기
- Mocking은 객체 간 결합을 낮추고, 인터페이스 기반 설계를 독려함.
- SOLID 원칙 중 DIP를 지키는 데에도 도움이 됨.
- 비즈니스 로직 검증에 집중
- 외부 시스템/라이브러리 동작까지 고려하지 않고, 순수하게 우리가 작성한 코드의 로직만 검증할 수 있으므로, "무엇이" 문제인지 빠르게 파악할 수 있다.
- ex) "EmailService가 과금 시스템과 어떻게 연동되는지"를 전부 통합 테스트하기 전에, "EmailService의 핵심 로직"만 Mock SMTP 서버로 테스트.
Mocking 사용시 주의사항
- 과도한 Mock 남발
- 모든 의존성을 Mock으로 대체하면, 실제 코드와의 동작 차이를 놓칠 수 있고, "너무" 분리된 환경에서만 테스트하는 리스크가 있음
- 통합 테스트나 E2E 테스트도 적절히 수행해야, 실제 시스템과의 동작이 정상임을 확인할 수 있음.
- Mock 객체의 잘못된 설정(Over-stubbing)
- 불필요한 많은 when() 조건을 설정하다 보면 테스트가 지나치게 세부 구현에 의존적이게 됨
- 코드 리팩토링 시 Mock 설정 부분이 깨질 확률이 높아짐.
- 테스트 코드 유지 보수
- Mock으로 인해 테스트가 복잡해지면, 실제 시스템이 아닌 "Mock 시뮬레이션"만 올바르게 돌고 있는 상황이 생김.
- 항상 "실제 로직"과 "Mock 설정"간에 갭이 없는지 점검이 필요함.
- 정확한 Mock 이름짓기
- "MockUserRepository", "StubPaymentService"와 같이 명시적인 네이밍을 쓰면, 테스트 코드를 읽는 사람이 "이건 가짜 구현체구나!" 라고 바로 인지 할수 있어 유지 보수에 좋음.
Test Double 용어 정리
이 용어는 스턴트 더블(stunt double)에서 차용한 것입니다. 즉, 테스트 중인 시스템의 일부분이 완전히 준비되지 않았거나 테스트하기 어려운 상황에서 그 대안으로 사용될 수 있는 '가짜' 컴포넌트를 의미합니다. 테스트 더블은 실제 객체의 행동을 모방하며, 테스트가 특정 조건과 상호작용을 쉽게 재현하고 검증할 수 있도록 합니다.
Dummy
- 정의: 아무 역할도 수행하지 않고, 오직 ‘자리 채우기’ 용도로만 사용.
- 설명: 메서드 시그니처가 특정 타입의 인자를 필요로 하지만, 그 인자를 실제로는 전혀 사용하지 않을 때.
- 예: new DummyUser() 객체를 넘기기만 하고, 실제론 해당 객체의 필드나 메서드를 전혀 사용하지 않을 때.
Fake
- 정의: 실제 구현과 유사하지만, 프로덕션 품질 수준은 아닌 간이 구현.
- 설명: 실제 DB 대신 In-memory DB 사용, 실제 캐시 대신 메모리 기반 자료구조 사용 등
- 예: H2 인메모리 DB를 사용하는 Repository 테스트는 실제 DB는 아니지만, DB처럼 동작하도록 구현한 Fake를 사용하는 것과 유사함.
Stub
- 정의: 호출에 대해 미리 정해진 답변을 반환하도록 설정된 객체. 특정 상황을 가정해서 결괏값을 고정시켜두는 것.
- 설명: 외부 의존성의 복잡한 로직을 테스트에서 분리하고, “고정된 응답”을 반환하도록 세팅할 때.
- 예: stubRepository.findUser() 호출 시 항상 특정 User 객체를 반환하게끔 설정.
Spy
- 정의: Stub의 기능(미리 정해진 답변 반환) + “어떤 메서드가 몇 번 호출되었는지, 어떤 파라미터로 호출되었는지” 기록까지 함께 해주는 객체.
- 설명: 실제 로직 흐름을 그대로 따르면서, “호출 이력”을 관찰해야 할 때. 예: 특정 결괏값을 반환하되, 중간에 로깅/호출 횟수 확인 등 부수적 확인이 필요한 경우.
Mock
- 정의: 호출 과정(함수 호출 횟수, 파라미터, 순서 등)에 대한 기대치(Expectation)를 미리 설정하고, 실제로 그렇게 호출됐는지 검증할 수 있는 객체.
- 설명: 외부 의존성을 완전히 대체하고, 행위 기반 검증(Behavior Verification)을 할 때 많이 사용. Mockito의 when(), verify() 등을 예로 들 수 있음.
'[내일배움캠프-Sparta] > Spring 6기' 카테고리의 다른 글
| TIL 43 [ Spring 심화 프로젝트 ( Day2 ) - 구현 ( 회원 가입, 전체 유저 조회, 회원 정보 수정, 암호화 처리 ) ] (0) | 2025.04.23 |
|---|---|
| TIL 42 - [ Spring 심화 프로젝트 ( Day 1 ) - 설계 ( 와이어프레임, ERD, API 명세서 ) ] (0) | 2025.04.22 |
| TIL 41 - [ Spring 심화 ( API 예외처리, JPQL, Fetch Join ) ] (0) | 2025.04.21 |
| TIL 40 - [ Spring 심화 ( HttpMethodConverter, Converter, Formatter ) ] (0) | 2025.04.15 |
| TIL 39 [ Spring 기초 프로젝트 ( D-day ) - 발표/마무리, 트러블슈팅 ] (0) | 2025.04.14 |