1. 테스트
UI 테스트의 문제점
- 하나의 테스트를 수행하는 데 참여하는 클래스와 코드가 너무 많음
- → 다른 계층의 코드/컴포넌트/서버 설정 등 많은 요소가 테스트에 영향
Unit Test
관심사의 분리
테스트는 가능한 한 최소 단위로 쪼개서 집중해야 함
단위
- 한 관심에 집중해 효율적으로 테스트할 만한 범위
- 작을수록 용이
- 단위를 넘어서는 코드는 신경 X 참여 X
- 확인의 대상과 조건이 간단하고 명확할수록 좋음
외부 리소스 의존 금지
- 리소스(DB, ..)의 상태를 테스트가 관장하면 가능
- 의존 : 리소스의 상태가 매번 변화, 테스트를 위해 DB를 특정 상태로 만들 수 없는 상태
선 단위테스트 → 후 플로우 테스트
- 단위별로 검증해 오류 잡은 후 긴 테스트를 하면 디버깅이 용이할 것
자동수행 테스트 코드
- 테스트의 전 과정을 자동으로 진행 ⇒ 자주 반복 가능!
- 테스트용 클래스는 별도로 만들기
지속적 개선과 점진적 개발
2. UserDaoTest 개선
UserDaoTest의 문제점
수동 확인 작업
- 콘솔은 값만 출력 ⇒ 일치 여부 확인은 사람의 책임
실행 작업의 번거로움
main()
메소드 이용하는 방법보다 좀 더 편리하고 체계적인 대안 필요
Fix: 테스트 검증의 자동화
실패의 종류
- 테스트 에러 : 테스트 진행 중 에러 발생
- 테스트 실패 : 에러는 없지만 결과가 기대한 바와 불일치
if (!user.getName().equals(user2.getName())) {
System.out.println("테스트 실패 : name 불일치");
}
Comprehensive Test
만들어진 코드의 기능을 모두 점검
좋은 테스트
- 빠르게 실행 가능
- 코드가 스스로 테스트 수행 + 기대하는 결과에 대한 확인 진행
Fix: 테스트의 효율적인 실행과 결과 관리
테스트 지원 도구 + 작성법
- 일정한 패턴 가진 테스트 생성
- 테스트 간단히 실행
- 결과 종합해서 열람
- 실패한 곳을 빠르게 찾음
JUnit : 자바 테스팅 프레임워크
- 프레임워크 : 개발자가 만든 클래스에 대해 IoC 수행 → 주도적으로 app 흐름 제어
- 오브젝트 생성, 실행
main()
불필요(일반 메소드 사용), 오브젝트 생성 후 실행하는 코드 불필요
- 요구 조건
- 메소드
public
으로 선언 - 메소드에
@Test
어노테이션 붙이기 - 리턴 값
void
- 매개변수 X
- 메소드
- 실패 여부 검증 기능
assertThat(user2.getName(), is(user.getName()));
- 성공 여부 직접 알려줌
- 실행
- 어디에든 main 메소드 추가
- JUnitCore 클래스의 main메소드 호출
- 인자 : @Test 메소드 가진 클래스명
inport org.junit.runner.JUnitCore; ... public static void main(String[] args) { JUnitCore.main("springbook.user.dao.UserDaoTest"); }
- 결과
- 성공 시: 실행 소요 시간, 테스트 결과, 실행한 테스트 메소드 갯수
- 실패 시
- OK 대신 FAILURES!!! 출력
- 실패한 테스트 개수
- 기대한 값, 실제 얻은 값
- 호출 스택
- assertThat() 실행한 경우 : AssertionError
3. JUnit
실행 방법
- JUnitCore 이용: main() 메소드 필요
- Run As JUnit 옵션 클릭 : 개별 클래스 단위 / 패키지 단위 / 프로젝트 단위 실행 가능
빌드 툴
- Maven : 빌드 툴에서 제공하는 JUnit 플러그인/태스크 이용 → HTML or TXT로 결과 제공
- 여러 명이 작성한 코드 통합 수행 시
- 서버에서 모든 코드 가져와 통합 및 빌드 : 빌드 스크립트 이용
- JUnit 테스트 수행
- 결과 메일로 통보받기
결과의 일관성
코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 함!
- 여러 번 돌렸을 때 성공하기도 하고 실패하기도 하면 좋은 테스트라 볼 수 없음
테스트 전 테스트 실행에 문제 되지 않는 상태 만들기
- DB 잔류 데이터 등 외부 환경에 영향 받지 말아야 함
실행 순서를 바꿔도 동일한 결과가 보장되어야 함
- 독립적으로 항상 동일한 결과 도출해야
포괄 테스트
- JUnit 은 한 클래스 안에 여러 개 테스트 메소드 들어가도록 허용
- 코드 수정 후에는 그 수정에 영향 받을 만한 테스트 실행해야 함
예외 테스트
네거티브 테스트를 먼저 만들기
- 부정적인 케이스를 먼저 만드는 습관 들여야 예외적인 상황 누락 방지 가능
TDD
- 만들어야 할 기능 결정
- 테스트 개발
- 추가하고 싶은 기능을 코드로 표현
테스트 = 기능 정의서
- 조건 : 어떤 조건을 가지고
- 행위 : 무엇을 할 때
- 결과 : 어떤 결과가 나온다
테스트 주도 개발
- 테스트 코드 먼저 작성 : 만들고자 하는 기능의 내용 + 검증 코드 포함
- 테스트 성공하게 해주는 코드 작성
- 테스트 작성 - 성공 코드 작성 주기를 가능한 한 짧게
- 단위 테스트 방식을 통해 작은 단위로 빠르고 간편하게 개발 진행
장점
- 코드에 대한 피드백 신속
- 코드 개발 착수 시 머릿속에서 일어나는 프로세스를 그대로 코드에 반영
- 머릿속으로 구상할 때보다 더 자유로운 조건에서 오류를 제어하고, 간편하게 반복 가능
- 테스트 코드 작성 시간이 적고 작성 방법 또한 간편 → 개발 속도 향상
테스트 코드 리팩토링
- 결과가 일정하게 유지되는 한 얼마든지 리팩토링 가능!
중복 코드 추출
- JUnit : 반복되는 준비 작업을 별도의 메소드에 넣고, 이를 매번 테스트 메소드 실행 전 먼저 실행시켜줌
- 일부 테스트 메소드만 공통적으로 사용하는 코드
- 메소드 추출 기법을 통해 분리한 후 테스트 메소드에서 직접 호출
- 공통적인 특징 지닌 테스트 메소드들 모아 별도의 테스트 클래스로 분리
JUnit의 테스트 메소드 실행 흐름
- 테스트 클래스에서 조건을 만족하는 테스트 메소드 모두 찾기
@Test
어노테이션public
메소드void
형- 매개변수 X
- 테스트 클래스 오브젝트 생성
- 하나의 테스트 메소드 사용 후 버려짐
- ∵ 독립 실행 보장
@Before
붙은 메소드 존재 시 실행@Test
붙은 메소드 한 개 실행 → 결과 저장@After
붙은 메소드 존재 시 실행- 나머지 테스트 메소드에 대해 2~5번 반복
- 모든 테스트의 결과 종합해 보고
Fixture
테스트를 수행하는 데 필요한 정보나 오브젝트
e.g. UserDaoTest의 dao, add() 메소드에 전달하는 User 오브젝트들
@Before
메소드 이용해 생성해두면 편리- 매번 새로운 테스트 오브젝트가 생성되므로 인스턴스 변수 선언 시 바로 초기화해도 상관 없으나,
픽스처 생성 로직을 모아두는 것이 더 깔끔
4. 스프링 테스트 적용
테스트 전체가 공유하는 오브젝트
@Before
메소드가 테스트 메소드 개수만큼 반복 ⇒ 어플리케이션 컨텍스트도 그만큼 생성- 애플리케이션 컨텍스트 생성 시 모든 싱글톤 빈 오브젝트 초기화
- 생성 시 자체적으로 초기화 작업까지 하는 빈도 있음
- 독자적으로 많은 리소스 할당하거나 독립적 스레드 띄우는 빈도 존재
- 테스트 마칠 때마다 컨텍스트 내 빈이 할당한 리소스 정리 필요
- ⇒ 빈이 많아지고 복잡해질 경우 생성 시간이 많이 소모됨!
- 생성에 많은 시간/자원 소모되는 오브젝트는 테스트 전체가 공유하도록 생성
- 최초 1회만 만들고 여러 테스트가 공유해서 사용
- 애플리케이션 컨텍스트 : 초기화 후 내부 상태가 바뀌는 일이 거의 없음
- 빈 : 싱글톤이므로 상태 X
- 스태틱 필드에 애플리케이션 컨텍스트 저장
- 매번 테스트 클래스 오브젝트 새로 만드는 문제 해결 (↔ 오브젝트 레벨 저장)
- 💡
@BeforeClass
: JUnit 제공 - 클래스전체에 걸쳐 딱 한 번 실행
- 스프링에서 제공하는 애플리케이션 컨텍스트 테스트 지원 기능이 더 편리
테스트를 위한 애플리케이션 컨텍스트 관리: 테스트 컨텍스트 프레임워크
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
...
@Before
public void setUp() {
this.dao = this.context.getBean("userDao", UserDao.class);
}
}
RunWith
: 한 클래스 內 테스트 메소드의 컨텍스트 공유
- 스프링의 JUnit 확장 기능 : 일종의 DI 원리
- 테스트 실행 전 단 1회 애플리케이션 컨텍스트 생성
- 테스트 오브젝트 생성 때마다, 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입
- 하나의 애플리케이션 컨텍스트가 만들어진 뒤, 모든 테스트 메소드에서 해당 컨텍스트를 공유해 사용
- 첫 번째 테스트의 실행 시간이 가장 긺
↔ JUnit : 테스트 메소드 실행할 때마다 새로운 테스트 오브젝트 생성
@ContextConfiguration
: 테스트 클래스의 컨텍스트 공유
여러 테스트 클래스가 모두 같은 설정파일 가진 애플리케이션 컨텍스트 사용 시
→ 클래스 간 애플리케이션 컨텍스트 공유!
@Autowired
스프링 DI에 사용
- 인스턴스 변수의 타입과 일치하는 빈을 컨텍스트에서 찾음
⇒ 존재 시 인스턴스 변수에 주입 - 생성자/수정자 메소드 없이도 주입 가능
- 타입에 의한 오토 와이어링 : 별도 DI 설정 없이 필드의 타입정보 이용해 빈을 자동으로 가져옴
ApplicationContext
타입 빈- 스프링 애플리케이션 컨텍스트는 초기화 시 자기 자신도 빈으로 등록
⇒ 애플리케이션 컨텍스트에 해당 타입의 빈이 존재하게 됨 - ∴ xml 등에 정의된 빈이 아님에도 DI 가능!
- 스프링 애플리케이션 컨텍스트는 초기화 시 자기 자신도 빈으로 등록
- 사용 시, DI받은 컨텍스트에서 다시 DL 방식으로 빈을 가져오지 않아도 됨!
애플리케이션 컨텍스트가 갖고 있는 빈을 직접 DI - 같은 타입 빈이 2개 이상인 경우 : 변수명과 동명의 빈이 있는지 확인 → 못 찾을 시 예외 발생
- 인터페이스 타입 vs 클래스 타입
- 가능한 한 인터페이스 사용해 애플리케이션 코드와 느슨하게 연결!
- 구현 클래스를 변경해도 테스트 코드는 유지할 수 있기 때문
- 단순히 인터페이스에 정의된 메소드 사용하고 싶을 때 - 인터페이스 타입
- 구현 클래스 타입의 오브젝트 자체에 관심이 있을 때 - 클래스 타입
- e.g. XML에서 프로퍼티로 설정한 DB 연결 정보 확인, 클래스의 메소드를 직접 이용해 테스트, ..
- 가능한 한 인터페이스 사용해 애플리케이션 코드와 느슨하게 연결!
테스트에 DI 이용
인터페이스로 DI 적용해야 하는 이유
- 절대로 바뀌지 않는 개발은 없음
- 다른 차원의 서비스 기능 도입 가능
- 새로운 기능 추가 시 기존 코드 수정 X, 삭제 시에도 설정 파일만 간단히 수정하면 됨
- AOP
- 효율적인 테스트의 손쉬운 제작
- 작은 단위로 테스트 생성 및 실행을 가능케 함
DI 적용된 코드는 테스트도 다양한 방식 활용 가능할만큼 유연함
테스트 중 DAO가 사용할 DataSource 오브젝트를 별도로 지정하기
(운영용 DB 사용 방지를 위함)
테스트 코드에 의한 DI
Sol 1) 테스트 코드에서 빈 오브젝트에 수동 DI
- 장점 : XML 수정 없이, 테스트 코드 통해 오브젝트 관계 재구성
- XML의 설정 정보를 따라 구성한 오브젝트를 가져와 의존관계 수동(강제) 변경
- 나머지 모든 테스트 수행하는 동안 변경된 컨텍스트를 사용해야 함
@DirtiesContext
이용으로 해결
- 테스트 내에서 애플리케이션 컨텍스트의 구성이나 상태를 변경하지 않는다는 원칙 위배
- 나머지 모든 테스트 수행하는 동안 변경된 컨텍스트를 사용해야 함
테스트를 위한 별도의 DI 설정
Sol 2) 테스트 전용 설정 파일 별도 제작 → @ContextConfiguration
의 location 엘리먼트 변경
- 장점 : 수동 DI 코드 불필요, @DirtiesContext 불필요
- 애플리케이션 컨텍스트 1개만 생성 후 모든 테스트에서 공유 가능
컨테이너 없는 DI 테스트
Sol 3) 테스트 코드에서 직접 오브젝트 생성 후 DI
public class UserDaoTest {
UserDao dao;
. . .
@Before
public void setUp() {
. . .
dao = new UserDao();
DataSource dataSource = new SingleconnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource);
}
}
- 스프링 테스트 컨텍스트 프레임워크, Autowired X
@Before
메소드에서 직접 UserDao 오브젝트 생성 → 테스트용 DataSource 오브젝트 생성 → 직접 DI - 장점 : 컨텍스트 미사용 ⇒ 코드 단순, 테스트 시간 절약
- 단점 : DataSource 직접 생성하는 번거로움, JUnit에 의해 매번 새로운 오브젝트 생성
- 컨테이너/프레임워크는 DI의 편리한 적용을 도우기만 할 뿐, 가능하게 해주는 것은 아님!
invasive 기술: 기술 적용 시 코드에 기술 관련 API 등장하거나, 특정 인터페이스/클래스 사용 강제
→ 코드가 해당 기술에 종속
non-invasive 기술 : 코드에 영향 X → 기술 독립적인 순수한 코드 유지 가능
e.g. 스프링
DI를 이용한 테스트 방법 선택
- 스프링 컨테이너 없이 테스트할 수 있는 방법을 최우선 고려
- 장점 : 수행 속도 신속, 테스트 간결
- 테스트 위해 필요한 오브젝트 생성 & 초기화 단순할 경우
- 스프링의 설정을 이용한 DI
- 여러 오브젝트와 복잡한 의존 관계를 갖고 있는 오브젝트 테스트할 경우
- 테스트에서 컨텍스트 사용 시 테스트 전용 XML 별도 생성
- 개발 환경(개발자 테스트 시 이 환경) / 테스트 환경 / 운영 환경
- 수동 DI
- 예외적인 의존 관계를 강제로 구성해서 테스트해야 할 경우
5. 학습 테스트로 배우는 스프링
Learning Test : 자신이 만들지 않은 프레임워크나 다른 팀에서 제공한 라이브러리에 대한 테스트 작성
- 목적 : 자신이 사용할 API / 프레임워크를 테스트로 보면서 사용법 습득
- 기능 검증이 목적이 아님!
- 기능/기술 이해도, 사용법 숙지 여부 검토가 목적
- 테스트 코드를 작성하며 신속정확하게 사용법 숙지 가능
장점
- 자동 테스트 통해 다양한 조건에 따른 기능 확인 용이
- 만든 코드를 개발 중에도 참고 가능
- 제품 업그레이드 시 호환성 검증 기능
- 테스트 작성 훈련
Bug Test
코드에 오류가 있을 때 그 오류를 가장 잘 드러내주는 테스트
- 코드를 뒤지며 트러블슈팅하는 것보다 유용
방법
- 테스트가 실패하도록 코드 생성 : 버그가 원인이 되도록 함
- 버그 테스트가 성공하도록 애플리케이션 코드 수정
필요성
- 테스트의 완성도 보완, 유사 문제 발생 시 추적 용이
- 버그 내용 명확히 분석
- 기술적 문제 해결 시 도움
Equivalence Partitioning
같은 결과를 내는 값의 범위를 구분 → 각 대표값으로 테스트 진행
e.g. {true, false, exception} : 각 결과를 내는 입력값 or 상황조합의 범위 만들기
Boundary Value Analysis
경계의 근사값 이용해 테스트
- 에러는 동등분할 범위의 경계에서 주로 발생한다는 특징 반영
'Group Study (2024-2025) > Spring 심화' 카테고리의 다른 글
[ Spring 심화 ] 6주차 - AOP(1) (0) | 2024.12.01 |
---|---|
[ Spring 심화 ] 5주차 - 서비스 추상화 (2) | 2024.11.06 |
[ Spring 심화 ] 4주차 - 예외처리 (1) | 2024.10.29 |
[ Spring 심화 ] 3주차 - 템플릿 (0) | 2024.10.29 |
[ Spring 심화 ] 1주차 - 오브젝트와 의존관계 (1) | 2024.10.01 |