Today I Learned_230506
토비의 스프링 3.1
스프링 테스트 적용
@Before는 테스트 메소드 개수만큼 반복 → 어플리케이션 컨텍스트도 세 번 만들어진다.
어플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화
→ 이와 같이 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.
빈은 싱글톤으로 만들어져있기에 상태를 갖지 않음
→ 어플리케이션 컨텍스트는 한 번만 만들고 여러 테스트가 공유해서 사용해도 된다.
@BeforeClass
- 테스트 클래스 전체에 걸쳐 딱 한번만 실행되는 스태틱 메소드
→ 스프링이 제공하는 어플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 편리함
간단한 어노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.
JUnit 테스트 컨텍스트 프레임워크를 사용하면 어노테이션 설정만으로 모든 테스트가 어플리케이션 컨텍스트를 공유하도록 할 수 있다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
JUnitCore.main("com.example.demo3.dao.UserDaoTest");
}
@Autowired
private ApplicationContext context;
@Before // @Test 메소드가 실행되기 전에 먼저 실행되어야 하는 메소드 정의
public void setUp() {
this.dao = context.getBean("userDao", UserDao.class);
- RunWith : JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션
- SpringJUnit4ClassRunner를 지정 → 테스트를 진행하는 중에 테스트가 사용할 어플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해 준다.
- @ContextConfiguration : 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치 지정
- JUnit 확장기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만든다
- 테스트 오브젝트가 만들어질 때마다 어플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입한다 (DI)
→ 하나의 테스트 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유해서 사용한다.
- 같은 설정 파일(appilcationContext.xml)을 사용한다면 테스트 수행 중에 단 하나의 어플리케이션 컨텍스트만 만들어진다.
- 다른 테스트 클래스 사이에서도 같은 어플리케이션 컨텍스트를 공유한다.
@Autowired
- 스프링의 DI에 사용되는 어노테이션. 자동 와이어링을 진행해 준다.
- @Autowired가 붙은 인스턴스 변수가 있으면, 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다.
- 같은 타입의 빈이 두 개 이상 있는 경우, 변수의 이름과 같은 이름의 빈이 있는지 확인
- 변수 이름으로도 빈을 찾을 수 없는 경우 예외 발생
스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록
→ 애플리케이션에는 ApplicationContext라는 타입의 빈이 존재하고, DI도 가능하다.
테스트는 필요하다면 얼마든지 어플리케이션 클래스와 밀접한 관계를 맺고 있어도 된다.
→ 꼭 필요하지 않다면 가능한 한 인터페이스를 사용해서 애플리케이션 코드와 느슨하게 연결해 두는 편이 좋다.
개발하는데 DI를 적용해야 하는 이유
- 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다.
- 당장 바꿀 계획이 없어도, 언제 어떻게 바뀔지 모른다.
- 클래스의 구현 방식이 바뀌지 않더라도, DI를 적용하면 다른 차원의 서비스 기능을 도입하기 쉬워진다.
- 예를 들면 DB 커넥션을 카운팅하는 기능.. DI가 들어간다면 구현하기 쉬워진다.
- 효율적인 테스트를 만들기 위함
- DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.
테스트 코드에 의한 DI
DI를 사용하여 테스트 중에 의존 관계를 바꿀 수 있다.
운영과 테스트의 환경이 다름 → 테스트 DI를 통해 해결할 수 있다.
예제
public void setUp() {
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "spring", "book", true);
장점
- XML 설정파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성 할 수 있음
단점
- 스프링 애플리케이션 컨텍스트는 싱글톤 빈이므로 테스트 중에 딱 한개만 만들어진다.
- 싱글톤이므로 상태 변경을 하지 않는 것이 원칙
- 의존관계를 강제로 변경 → 한번 변경하게 되면 나머지 테스트가 변경한 내용을 그대로 사용할 것
- 이렇게 수동으로 DI하는 것은 장점보다 단점이 많다.
해결 방법 → @DirtiesContext 사용
- 스프링 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태가 변경됨을 알려줌
- 이 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않음
- 테스트 메소드 수행 후, 매번 새로운 애플리케이션 컨텍스트 생성
- 변경한 컨텍스트가 다음 테스트에 영향을 주지 않기 위함
- 메소드 레벨에도 적용 가능
테스트를 위한 별도의 DI 설정
- 테스트 전용 어플리케이션 컨텍스트 설정 파일(applicationContext.xml)을 따로 만드는 것.
컨테이너 없는 DI 테스트
- 스프링 컨테이너를 사용하지 않고 테스트를 만드는 것
- UserDaoTest는 스프링 컨테이너가 제대로 동작하는지 관심을 가질 필요가 없다.
- 직접 객체를 생성하고 DI를 진행한다.
예시
// @Autowired 제거
private UserDao dao;
@Before // @Test 메소드가 실행되기 전에 먼저 실행되어야 하는 메소드 정의
public void setUp() {
dao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "root", "", true);
dao.setDataSource(dataSource);
- 코드가 단순해지고 이해하기 편해짐
- 매번 UserDao 오브젝트가 새로 만들어지긴 하지만, 가벼운 오브젝트이므로 부담이 되지 않는다.
침투적 기술과 비침투적 기술
침투적 기술 (Invasive)
- 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API가 등장
- 특정 인터페이스나 클래스를 사용하도록 강제
- 애플리케이션 코드가 해당 기술에 종속된다.
비침투적 기술 (noninvasive)
- 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다.
- 기술에 종속적이지 않은 순수한 코드의 유지가 가능
- 스프링은 비침투적인 기술의 대표적인 예
DI를 이용한 테스트 방법
- 스프링 컨테이너 없이 테스트 할 수 있는 방법
- 객체를 직접 만들고, DI도 직접 하자
- 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.
- 스프링의 설정을 이용한 DI방식의 테스트
- 여러모로 복잡한 의존관계를 가지고 있을 때 사용
- 테스트 전용 설정 파일을 따로 만든다.
- 수동 DI 테스트
- 예외적인 의존관계를 강제로 구성해야 할 경우
- @DirtiesContext 어노테이션 필요
학습 테스트
자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리에 대한 테스트
학습 테스트의 목적
- 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 것
- 기능 검증이 목적은 아니다.
- 기술이나 기능에 대해 얼마나 이해하고 있는지, 사용 방법을 제대로 알고 있는지 확인하는 것이 목적
학습 테스트의 장점
- 다양한 조건에 따른 기능을 손쉽게 확인해 볼 수 있다.
- 자동화된 테스트로 만들어지므로 손쉽게 확인 가능
- 학습 테스트 코드를 개발 중에 참고할 수 있다.
- 실제 개발 코드에서 샘플로 참고 가능
- 프레임워크나 제품을 업그레이드 할 때 호환성 검증을 도와준다
- 기존에 사용하던 API 기능에 변화가 있거나 업데이트 되었는지 확인 가능
- 테스트 작성에 대한 좋은 훈련이 된다
- 실제로 프레임워크를 사용하는 애플리케이션 코드의 테스트와 유사하므로 테스트 작성에 대한 훈련이 된다.
- 새로운 기술을 공부하는 과정이 즐거워진다
스프링 자신에 대한 테스트 코드 → 스프링 학습 테스트를 만들 때 참고할 수 있는 가장 좋은 소스
학습 테스트 예제 - JUnit 테스트 오브젝트 테스트
JUnit은 테스트 메소드를 수행할 때 마다 새로운 오브젝트를 만드는지 확인
JUnit으로 만드는 JUnit 자신에 대한 테스트
package com.example.demo3.learningtest.junit;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import org.junit.Test;
public class JUnitTest {
static JUnitTest testObject;
@Test
public void test1() {
assertThat(this, is(not(sameInstance(testObject))));
testObject = this;
}
@Test
public void test2() {
assertThat(this, is(not(sameInstance(testObject))));
testObject = this;
}
@Test
public void test3() {
assertThat(this, is(not(sameInstance(testObject))));
testObject = this;
}
}
- not() : 뒤에 나오는 결과를 부정하는 매처
- sameInstance() : 실제로 같은 오브젝트인지 비교 → 동일성 비교 매처
- 이 테스트의 단점 → 바로 직전의 오브젝트만 비교
개선
static Set<JUnitTest> testObjects = new HashSet<JUnitTest>();
@Test
public void test1() {
assertThat(testObjects, not(hasItem(this)));
testObjects.add(this);
}
- hasItem() : 컬렉션의 원소인지를 검사하는 매처
assertThat(contextObject == null || contextObject == this.context, is(true));
assertTrue(contextObject == null || contextObject == this.context);
assertThat(contextObject, either(is(nullValue())).or(is(this.context)));
- is()는 타입만 맞으면 어떤 값이든 검증할 수 있다.
- assertTrue() : 검증용 메소드
- either() : 뒤에 이어서 나오는 or()와 함께 두 개의 매처의 결과를 or조건으로 비교
- nullValue() : 오브젝트가 null인지 확인
버그 테스트
코드에 오류가 있을 때 그 오류를 가장 잘 드러내 줄 수 있는 테스트
일단 실패하도록 만들어야 한다. 버그가 원인이 되어서 테스트가 실패하는 코드를 만들자.
장점
- 테스트의 완성도를 높여준다
- 버그의 내용을 명확하게 분석하게 해 준다
- 기술적인 문제를 해결하는 데 도움이 된다
동등 분할 : 같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법
경계값 분석 : 에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해 경계값 근처에 있는 값을 이용해 테스트