F-LAB JAVA · 6주차 · Phase 1 · JUnit 테스트
🏆 Phase 1 완주 — 일관된 테스트의 완성
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
픽스처(Fixture) 는 여러 테스트가 공통으로 쓰는 준비물(정보·오브젝트)로 @BeforeEach 에서 매 테스트 전에 새로 생성하며, 테스트는 코드 변경이 없으면 항상 같은 결과를 내야 하므로 DB 잔여 데이터로 인한 실패를 막기 위해 시작 상태를 보장해야 한다.
픽스처 는 테스트 수행에 필요한 정보·오브젝트로, 여러 테스트가 공통으로 쓰는 준비물이다.
픽스처는 인스턴스 변수로 선언하고 @BeforeEach 에서 매 테스트 전에 새로 생성하여, 중복을 제거하고 각 테스트가 깨끗한 픽스처로 시작하게 한다.
좋은 테스트는 코드 변경이 없으면 항상 같은 결과 를 내야 하는데, DB 에 이전 테스트의 잔여 데이터가 남아 실패한다면 그것은 잘못된 테스트다.
따라서dao.deleteAll()같은 코드로 시작 상태를 보장 하거나, 통합 테스트에서는@Transactional(테스트 후 롤백) ·@Sql(초기 데이터) 로 DB 를 격리하여 "테스트는 외부 환경에 의존하면 안 된다" 는 원칙을 지킨다.
픽스처 = 실험 표준 세팅:
픽스처 (준비물):
- 비커, 시약, 측정기
- 여러 실험 공통
- 실험 전 준비
@BeforeEach (매 실험 전):
- 비커 세척
- 시약 새로 준비
- 깨끗한 시작
일관성:
- 같은 조건 = 같은 결과
- 비커에 이전 잔여물 → 오염 (실패)
시작 상태 보장:
- 비커 세척 (deleteAll)
- 깨끗한 시작
통합 테스트 (실제 DB):
- @Transactional: 실험 후 원상복구 (롤백)
- @Sql: 초기 시약 세팅
외부 의존 X:
- 옆 실험실 상태 무관
- 독립 실험
→ 픽스처 = 공통 준비물 (@BeforeEach 생성), 시작 상태 보장 (일관성, 외부 의존 X).
1. 픽스처의 정의
2. @BeforeEach로 생성
3. 인스턴스 변수로
4. 테스트 결과의 일관성
5. DB 잔여 데이터 실패
6. 시작 상태 보장
7. 통합 테스트 격리
8. Phase 1 완주 정리
9. 면접 + 자기 점검
픽스처 (Fixture):
테스트 수행에 필요한
정보·오브젝트.
- 공통 준비물
- 여러 테스트가 사용
픽스처 예시:
- 테스트 대상 객체 (dao)
- 테스트 데이터 (sample)
- Mock 객체
- 설정 값
공통 준비물:
여러 테스트:
- 같은 dao 필요
- 같은 샘플 필요
→ 픽스처로 공유 (생성은 매번)
class ShipmentDaoTest {
// 픽스처 (공통 준비물)
private ShipmentDao dao; // 대상 객체
private Shipment sampleShipment; // 테스트 데이터
@BeforeEach
void setUp() {
dao = new ShipmentDao();
sampleShipment = new Shipment(1L, "BL001", BigDecimal.TEN);
}
@Test
void test_add() {
dao.add(sampleShipment); // 픽스처 사용
}
@Test
void test_get() {
dao.add(sampleShipment); // 픽스처 사용 (새것)
assertThat(dao.get(1L), is(notNullValue()));
}
// dao, sampleShipment = 픽스처
}
class ShipmentDao {
void add(Shipment s) {}
Shipment get(Long id) { return null; }
}
record Shipment(Long id, String blNo, java.math.BigDecimal weight) {}
픽스처(Fixture)의 정의는?
답:
1. 픽스처:
예시:
공통:
생성:
// @BeforeEach 로 픽스처 생성
class DaoTest {
private ShipmentDao dao; // 픽스처
@BeforeEach
void setUp() {
dao = new ShipmentDao(); // 매 테스트 전 새로
}
}
중복 제거:
픽스처 생성:
- @BeforeEach 한 곳
- 모든 테스트 공통
→ 각 테스트 중복 X
깨끗한 픽스처:
매 테스트 전:
- 새 픽스처
- 이전 영향 X
→ 깨끗한 시작
class FreightServiceTest {
// 픽스처
private FreightService service;
private ShipmentDao mockDao;
private FreightCalculator mockCalc;
@BeforeEach
void setUp() {
// 매 테스트 전 픽스처 생성 (중복 제거)
mockDao = mock(ShipmentDao.class);
mockCalc = mock(FreightCalculator.class);
service = new FreightService(mockDao, mockCalc);
}
@Test
void test_one() {
// 픽스처 사용 (깨끗)
service.process(new Shipment(1L));
}
@Test
void test_two() {
// 새 픽스처 (test_one 영향 X)
service.process(new Shipment(2L));
}
// setUp 한 곳, 모든 테스트 공통
}
class FreightService {
FreightService(ShipmentDao d, FreightCalculator c) {}
void process(Shipment s) {}
}
class ShipmentDao {}
class FreightCalculator {}
record Shipment(Long id) {}
@BeforeEach로 픽스처 생성하는 방식은?
답:
1. 매 전 생성:
중복 제거:
깨끗한 픽스처:
공통:
픽스처 = 인스턴스 변수:
private 필드로:
- @BeforeEach 에서 초기화
- @Test 에서 사용
→ 테스트 간 공유 (선언)
왜 인스턴스 변수:
- @BeforeEach 에서 할당
- @Test 에서 접근
- 여러 메서드 공유 (선언)
→ 지역 변수면 공유 불가
부담 없음:
인스턴스 변수 픽스처:
- 매번 새 인스턴스
- 초기화 보장
→ Unit 1.4 연결
class ShipmentServiceTest {
// 픽스처 = 인스턴스 변수
private ShipmentService service; // 인스턴스 변수
private ShipmentDao dao; // 인스턴스 변수
@BeforeEach
void setUp() {
dao = mock(ShipmentDao.class); // 할당
service = new ShipmentService(dao);
}
@Test
void test1() {
service.process(...); // 접근
}
@Test
void test2() {
verify(dao).add(...); // 접근 (같은 픽스처 선언)
}
// service, dao 를 여러 테스트에서 접근
// → 인스턴스 변수 (지역이면 불가)
}
class ShipmentService {
ShipmentService(ShipmentDao d) {}
void process(Object o) {}
}
class ShipmentDao { void add(Object o) {} }
픽스처를 인스턴스 변수로 두는 이유는?
답:
1. 인스턴스 변수:
왜:
공유:
부담 없음:
테스트 결과 일관성:
코드 변경 없으면:
- 항상 같은 결과
- 통과는 항상 통과
- 실패는 항상 실패
→ 신뢰
왜 중요:
일관성 없으면:
- 가끔 통과/실패 (flaky)
- 신뢰 X
- 무시하게 됨
→ 테스트 무의미
일관성 깨는 요인:
- DB 잔여 데이터
- 외부 시스템 의존
- 시간/날짜 의존
- 랜덤 값
- 테스트 순서
// 일관성 있는 테스트
class ConsistentTest {
private ShipmentDao dao;
@BeforeEach
void setUp() {
dao = new ShipmentDao();
dao.deleteAll(); // 일관성 (깨끗한 시작)
}
@Test
void add_one_shipment() {
dao.add(new Shipment(1L));
assertThat(dao.count(), is(1)); // 항상 1
// 몇 번 돌려도 항상 통과
}
}
// ❌ 일관성 없는 테스트
class FlakyTest {
@Test
void uses_current_time() {
// 시간 의존 (일관성 X)
LocalTime now = LocalTime.now();
assertThat(now.getHour(), is(lessThan(12)));
// 오후엔 실패 (flaky)
}
}
class ShipmentDao {
void add(Shipment s) {}
void deleteAll() {}
int count() { return 0; }
}
record Shipment(Long id) {}
테스트 결과의 일관성이란?
답:
1. 일관성:
왜 중요:
깨는 요인:
신뢰:
DB 잔여 데이터:
이전 테스트 데이터:
- DB 에 남음
- 다음 테스트 영향
- 잘못된 실패
// 잔여 데이터 문제
@Test
void test_count() {
dao.add(new Shipment(1L));
assertThat(dao.count(), is(1));
// 첫 실행: 통과
// 재실행: count = 2 (이전 데이터) → 실패!
}
// 잔여 데이터로 실패
잘못된 테스트:
잔여 데이터로 실패:
- 코드는 정상
- 테스트가 잘못
- 시작 상태 미보장
→ 테스트 설계 문제
근본 원인:
외부 상태 (DB):
- 인스턴스처럼 자동 격리 X
- 영속적
- 명시적 초기화 필요
// 잔여 데이터 문제와 해결
class ShipmentDaoTest {
private ShipmentDao dao;
// ❌ 초기화 없으면
@Test
void without_cleanup() {
dao.add(new Shipment(1L));
assertThat(dao.count(), is(1));
// 재실행 시 누적 → 실패
}
// ✓ 초기화 있으면 (다음 섹션)
@BeforeEach
void setUp() {
dao = new ShipmentDao();
dao.deleteAll(); // 잔여 제거
}
@Test
void with_cleanup() {
dao.add(new Shipment(1L));
assertThat(dao.count(), is(1)); // 항상 1
}
}
class ShipmentDao {
void add(Shipment s) {}
void deleteAll() {}
int count() { return 0; }
}
record Shipment(Long id) {}
DB 잔여 데이터로 인한 실패는?
답:
1. 잔여 데이터:
예시:
잘못된 테스트:
원인:
시작 상태 보장:
테스트 시작 시:
- 알려진 상태
- 깨끗한 시작
dao.deleteAll() 등
// 시작 상태 보장 (deleteAll)
@BeforeEach
void setUp() {
dao = new ShipmentDao();
dao.deleteAll(); // 모든 데이터 삭제 (깨끗)
}
// 매 테스트가 빈 DB 에서 시작
알려진 상태:
방법:
1. deleteAll (빈 상태)
2. 초기 데이터 삽입
3. 알려진 시작점
→ 예측 가능
일관성 확보:
시작 상태 보장:
- 매번 같은 시작
- 같은 결과
- 일관성
→ 신뢰 회복
class ShipmentDaoStateTest {
private ShipmentDao dao;
@BeforeEach
void setUp() {
dao = new ShipmentDao();
// 시작 상태 보장
dao.deleteAll(); // 1. 깨끗하게
dao.add(new Shipment(1L)); // 2. 알려진 초기 데이터
dao.add(new Shipment(2L));
// → 항상 2개로 시작 (알려진 상태)
}
@Test
void initial_count_is_two() {
assertThat(dao.count(), is(2)); // 항상 2
}
@Test
void add_makes_three() {
dao.add(new Shipment(3L));
assertThat(dao.count(), is(3)); // 2 + 1
// 알려진 시작 (2) 기반
}
// 매번 같은 시작 → 일관성
}
class ShipmentDao {
void add(Shipment s) {}
void deleteAll() {}
int count() { return 0; }
}
record Shipment(Long id) {}
시작 상태 보장 (deleteAll) 은?
답:
1. 시작 상태 보장:
deleteAll:
알려진 상태:
일관성:
통합 테스트 문제:
실제 DB 사용:
- 데이터 영속
- 테스트 간 영향
- 격리 어려움
// @Transactional (테스트 후 롤백)
@SpringBootTest
@Transactional // 각 테스트 후 자동 롤백
class ShipmentDaoIntegrationTest {
@Autowired
private ShipmentDao dao;
@Test
void test_add() {
dao.add(new Shipment(1L));
// 테스트 후 롤백 → DB 원상복구
}
}
// 변경이 커밋 안 됨 (격리)
// @Sql (초기 데이터)
@Sql("/test-data.sql") // 테스트 전 실행
@Test
void test_with_data() {
// test-data.sql 의 초기 데이터로 시작
assertThat(dao.count(), is(greaterThan(0)));
}
// 알려진 초기 상태
격리 방법:
@Transactional:
- 테스트 후 롤백
- 빠름, 편함
- 커밋 동작 테스트 X
@Sql:
- 초기 데이터 세팅
- 명시적
deleteAll (@BeforeEach):
- 수동 초기화
- 단순
// 통합 테스트 격리 (ILIC)
@SpringBootTest
@Transactional // 각 테스트 후 롤백 (격리)
class ShipmentDaoIntegrationTest {
@Autowired
private ShipmentDao dao;
@Test
@Sql("/init-shipments.sql") // 초기 데이터
void find_by_status() {
List<Shipment> booked = dao.findByStatus("BOOKED");
assertThat(booked, hasSize(2)); // 알려진 초기 데이터
// 테스트 후 자동 롤백 (다른 테스트 영향 X)
}
@Test
void add_and_verify() {
dao.add(new Shipment(99L, "BL099", "BOOKED"));
assertThat(dao.get(99L), is(notNullValue()));
// 테스트 후 롤백 → DB 깨끗
}
// @Transactional 로 격리 + @Sql 로 초기 상태
}
class ShipmentDao {
void add(Shipment s) {}
Shipment get(Long id) { return null; }
java.util.List<Shipment> findByStatus(String s) { return null; }
}
record Shipment(Long id, String blNo, String status) {}
통합 테스트 DB 격리 (@Transactional, @Sql) 는?
답:
1. @Transactional:
@Sql:
비교:
목적:
Phase 1 — JUnit 테스트
Unit 1.1 — 단위 테스트 필요성
- 코드가 코드 검증
- FIRST
Unit 1.2 — assertThat과 매처
- Hamcrest, is()
Unit 1.3 — JUnit 실행 방식
- 7단계, 새 인스턴스
Unit 1.4 — 매번 새 오브젝트
- 독립 실행, @BeforeAll
Unit 1.5 — 픽스처와 @BeforeEach
- 공통 준비물, 일관성
Phase 1 핵심 메시지:
"테스트는 코드가 코드를 자동 검증하며,
각 테스트는 독립적·일관적이어야 한다.
픽스처는 @BeforeEach 로 매번 준비하고,
외부 상태는 명시적으로 격리한다."
"테스트는 외부 환경에 의존하면 안 된다":
- 다른 테스트 (격리)
- DB 잔여 데이터 (초기화)
- 시간/랜덤 (제어)
- 외부 시스템 (Mock)
→ 일관성·독립성
Phase 1 → Phase 2:
- 테스트 → 웹 인프라
Phase 2 — 웹 인프라 기초:
- 웹서버 vs WAS
- 서블릿/JSP
- SSR vs CSR
- JAR vs WAR
Phase 1의 종합은?
답:
1. 5 Unit:
메시지:
원칙:
다음:
| Q | 핵심 답변 |
|---|---|
| 픽스처? | 공통 준비물 |
| @BeforeEach 생성? | 매 전, 중복 제거 |
| 인스턴스 변수? | 여러 메서드 공유 |
| 일관성? | 같은 결과 |
| 잔여 데이터? | 잘못된 실패 |
| 시작 상태? | deleteAll |
| @Transactional? | 롤백 |
| @Sql? | 초기 데이터 |
| 외부 의존? | 격리 |
| flaky? | 일관성 깨짐 |
답:
답:
답:
답:
답:
1. 픽스처와 @BeforeEach
2. 테스트 일관성
3. 외부 환경 격리
🧪 Phase 1 — JUnit 테스트
✅ Unit 1.1 단위 테스트의 필요성
✅ Unit 1.2 assertThat과 매처
✅ Unit 1.3 JUnit 실행 방식
✅ Unit 1.4 매번 새 오브젝트
✅ Unit 1.5 픽스처와 @BeforeEach ← 여기, Phase 1 완주
→ 테스트 필요성 (FIRST)
→ 단언 (assertThat/매처)
→ 실행 방식 (새 인스턴스)
→ 독립 실행
→ 픽스처 (일관성)
📚 Phase 2 — 웹 인프라 기초
Unit 2.1 — 웹서버 vs WAS
Unit 2.2 — 서블릿과 JSP
Unit 2.3 — SSR vs CSR
Unit 2.4 — JAR vs WAR
🧪 Part A — 학습 도구와 환경
✅ Phase 1 — JUnit 테스트 (5 Unit) ← 완주
⏭ Phase 2 — 웹 인프라 기초 (4 Unit)
총: 5/28 Unit
🏆 Phase 1 완주 — JUnit 테스트