본문 바로가기

토비의 스프링 정리

토비의 스프링 - 2.4 스프링 테스트 적용

2.4.1 기존 테스트의 문제점

  • @Test 애노테이션이 붙은 메소드를 테스트 할 때 마다 매번 새로운 오브젝트가 만들어지기 때문에, 매번 새로운 애플리케이션 컨텍스트가 생성됨
  • 애플리케이션 컨텍스트에 있는 빈들이 초기화 시점에 많은 리소스를 할당하거나 스레드를 띄우는 경우가 발생해 테스트 안정성 및 속도가 떨어짐
  • 빈은 싱글톤으로 만들어지기 때문에 상태를 가지지 않음(add(), get()을 한다고 해도 빈의 상태가 변경되지는 않음)
  • 때문에 애플리케이션 컨텍스트를 매번 만들지 않고 공유를 해도 상관 없음
  • @BeforeClass 애노테이션은 테스트 클래스 전체에 딱 한번만 생성되는 스태틱 메소드를 만듦
  • 스태틱으로 애플리케이션 컨텍스트를 저장해서 사용해도 되지만, 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하면 편리함

2.4.2 스프링 테스트 컨텍스트 프레임워크 적용

  • 적용방법
    1. ApplicationContext 타입의 인스턴스 변수 선언
    2. 생성한 인스턴스 변수에 @Autowired 애노테이션 추가
    3. @RunWith와 @ContextConfiguration 애노테이션 추가
  • @RunWith는 JUnit 프레임워크의 테스트 실행 방법확장할 때 사용. SpringJUnit4ClassRunner.class을 확장 클래스로 지정하면 테스트가 사용할 애플리케이션 컨텍스트만들고 관리하는 작업 진행
  • @ContextConfiguration는 자동으로 만들 컨텍스트의 설정파일 위치 지정
  • 밑과 같은 코드를 적용하면 동일 클래스 내부의 모든 테스트가 같은 애플리케이션 컨텍스트를 공유해서 사용
  • JUnit 확장기능은 테스트가 실행되기 전에 딱 한번만 애플리케이션 컨텍스트를 만들고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 컨텍스트를 특정 필드에 주입
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {
		...
    @Autowired
    private ApplicationContext applicationContext;

    @Before
    public void setUp(){
//        ApplicationContext applicationContext =
//                new GenericXmlApplicationContext("applicationContext.xml");
        this.dao = this.applicationContext.getBean("userDao", UserDao.class);
				...
    }
		...
}

💡 ApplicationContext을 DI 받고 DL방식(applicationContext.getBean())으로 빈을 가져오는 방식임

 

2.4.3 테스트 클래스의 컨텍스트 공유

  • @ContextConfiguration의 locations에 동일 설정파일을 할당
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class GroupTest {}

2.4.4 @Autowired

  • 스프링의 DI에 사용되는 특별한 애노테이션
  • @Autowired 애노테이션이 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크변수 타입일치하는 컨텍스트 내의 빈을 찾아 주입함
  • 스프링 애플리케이션 컨텍스트는 시작 및 초기화 할 때 자기 자신도 빈으로 등록하기 때문에 ApplicationContext가 빈으로 등록되지 않아도 가져올 수 있음
@Autowired 
private ApplicationContext applicationContext;
  • ApplicationContext가 아닌 빈을 직접 주입받는 것이 더욱 깔끔함
@Autowired
UserDao userDao;

2.4.5 DI와 테스트

  • 개발에서 절대 바뀌지 않는 것은 없기 때문에, DI를 통해 적은 노력으로 유지보수가 쉬운 코드 개발 가능
  • 인터페이스를 두고 DI를 적용하면 다른 차원의 서비스 기능을 도입할 수 있음(구현하는 메소드에 기능을 추가하면 됨)
  • DI를 적용하면 테스트를 손쉽게 만들 수 있음

2.4.6 테스트 코드에 의한 DI

  • DI는 애플리케이션 컨텍스트와 같은 스프링 컨테이너에만 할 수 있는것이 아님(스프링을 적용하기 전에 DaoFactory를 통해 DI를 구현해 봤음)
  • DI에 사용되는 수정자 메소드(Setter)는 평범한 자바 메소드 이므로, 테스트 코드에서 얼마든지 호출 가능
  • DI에 사용되는 수정자 메소드를 사용하여 테스트 코드에서 수동적으로 DI를 할 수 있음
  • 테스트용 클래스에서 수동적 DI를 통해 테스트용 DB 연결을 바꿀 수 있음
  • 💡 지금까지 테스트할 때도 서비스와 동일한 DB를 사용하고 있었음. 이제부터 테스트용 DB를 따로 만들어서 쉽게 연결할 수 있도록 할 것임
  • 이 방법은 애플리케이션 컨텍스트의 상태를 강제로 변경시킴(구성이나 상태를 변경하지 않는 것이 원칙임)
  • @RunWith을 통해 단 하나만의 애플리케이션 컨텍스트가 만들어지지만, 상태를 변경하면 애플리케이션 컨텍스트를 공유하는 다른 테스트 메소드 수행에 문제가 발생 할 수 있음
  • 때문에, @DirtiesContext를 사용하여 애플리케이션 컨텍스트를 공유하지 않게 하고 각 테스트마다 고유의 애플리케이션 컨텍스트를 생성하게 함
  • 이 방법은 또다시 테스트 메소드를 수행할 때 마다 애플리케이션 컨텍스트를 생성하여 리소스를 낭비하는 문제 발생
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
@DirtiesContext // RunWith가 있어도 테스트 각각 애플리케이션 컨텍스트 새로 생성함
public class UserDaoTest {
		...
    @Autowired
    UserDao dao;

    @Before
    public void setUp(){
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql://localhost/test?serverTimezone=UTC",
                "root",
                "1234",
                true
        );
        dao.setDataSource(dataSource);
        ...
    }
    ...
}

2.4.7 테스트를 위한 별도의 DI 설정

  • applicationContext.xml과 test-applicationContext.xml로 분리
  • 서비스용테스트용 설정파일을 분리하여 빈으로 등록하게 하면됨
  • 하나는 서버에서 운영용으로 사용하면 되고, 다른 하나는 테스트에 적합하게 준비된 설정파일을 사용하면 됨
  • @DirtiesContext 필요 없음
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserDaoTest {
    ...
}

2.4.8 컨테이너 없는 DI 테스트

  • 스프링 컨테이너에서 UserDao가 동작함을 확인하는 것은 UserDaoTest의 관심사가 아님
  • 또한 UserDao와 Datasource의 구현 클래스에서 스프링의 API를 직접적으로 사용하지 않기 때문에, 스프링 DI 컨테이너에 의존하지 않음
  • 이러한 이유로 컨테이너 없이 테스트를 구현할 수 있음
  • DI를 통한 개발로, UserDao가 스프링의 API에 의존하지 않고 자신의 관심에만 집중해서 만들어진 깔끔한 코드로 만들어 질 수 있었음
  • DI를 위한 컨테이너가 반드시 필요한 것은 아님
  • DI는 단순히 객체지향 프로그래밍 스타일
  • DI 컨테이너나 프레임워크는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 해주는 것은 아님
public class UserDaoTest {
    UserDao dao;

    @Before
    public void setUp(){
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql://localhost/toby?serverTimezone=UTC",
                "root",
                "1234",
                true
        );
        dao.setDataSource(dataSource);
        }
}

2.4.9 침투적과 비침투적 기술

  1. 침투적(Invasive) 기술
    • 애플리케이션 코드에 기술 관련 API가 등장하거나, 특정 인터페이스나 클래스를 사용하도록 강제하는 기술
  2. 비침투적(Noninvasive) 기술
    • 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용 가능
    • 종속적이지 않은 순수한 코드 유지 가능
    • 스프링은 이런 비침투적인 기술의 대표적 예시임
    • 때문에, 스프링 컨테이너가 없는 DI 테스트 가능

2.4.10 DI를 이용한 테스트 방법 선택

  • 항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 우선적 고려
  • 여러 오브젝트와 복잡한 의존관계를 갖고 있으면 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편함
  • 보통 개발환경과 테스트환경, 운영환경 차이가 있기 때문에 각각 다른 설정 파일을 만들어서 사용하는 경우가 일반적
  • 예외적인 의존관계를 강제로 구성할 경우 수동적 DI 방식을 사용하면 됨