토비의 스프링 Chapter 2.4 ~ 2.5 정리

종명·2021년 4월 24일
0

2.4 스프링 테스트 적용


public class UserDaoTest {
    private UserDao dao;
    
    @Before 
    public void setUp() {
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.dao = context.getBean("userDao", UserDao.class); 
    }
    
    @Test 
    public void addAndGet() throws SQLException {
        ...
    }
    
    @Test 
    public void count() throws SQLException {
        ...
    }
    
    @Test(expected=EmptyResultDataAccessException.class) 
    public void getUserFailure() throws SQLException {
        ...
    } 
}
  • @Before 메소드가 테스트 메소드 개수 만큼 반복되기 때문에 애플리케이션 컨텍스트도 3번 만들어진다. 빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 걸릴 수 있다.
  • 테스트는 가능한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다. 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.
  • JUnit은 테스트 클래스 전체에 걸쳐 딱 한번만 실행되는 @BeforeClass 스태틱 메소드를 지원한다. 하지만 이보다는 스프링이 직접 제공해주는 어플리케이션 컨텍스트 테스트 지원기능을 사용하는 것 더 편리하다.

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

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;

    @Before
    public void setUp() {
        this.dao = context.getBean("userDao", UserDao.class);
    }
    ...
}
  • @RunWith: JUnit 프레임워크의 테스트 실행 방법을 확장
  • @ContextConfiguration: 에플리케이션 컨텍스트의 위치 지정
    @Before
    public void setUp() {
        System.out.println(this.context);
        System.out.println(this);
        ...
    }
org.springframework.web.context.support.GenericWebApplicationContext@157fb63f
com.tobyspring.dao.UserDaoTest@36151fdb
org.springframework.web.context.support.GenericWebApplicationContext@157fb63f
com.tobyspring.dao.UserDaoTest@27a82eae
org.springframework.web.context.support.GenericWebApplicationContext@157fb63f
com.tobyspring.dao.UserDaoTest@5aabb5c3
  • setUp 메소드에 context와 현재 테스트 오브젝트를 출력해보았다.
  • 예상한대로 context는 세번 모두 동일하고, Junit은 테스트 메소드를 실행할 때마다 새로운 오브젝트를 만들기에 세번 모두 다르다.

장점

  1. 처음에 애플리케이션 컨텍스트가 만들어질때 시간이 오래 소모되고, 그 이후에는 애플리케이션을 재사용하기에 테스트 실행시간이 짧아진다.
  2. 여러개의 테스트 클래스가 있어도 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면, 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.
  3. @Autowired 를 이용한 주입이 가능하다. 일반적으로 주입을 위해서는 생성자나, 수정자가 필요하지만, 이 경우에는 메소드 없이도 주입이 가능하다. 이를 타입에 의한 자동 와이어링 이라한다.
    • 참고로 스프릥 애플리케이션 컨텍스트는 초기화 할 때 자기 자신도 빈으로 등록하기 때문에 ApplicationContext 타입의 빈이 주입이 가능한 것이다.

public class UserDaoTest {
    @Autowired
    UserDao userDao;
    ...
}

어플리케이션 컨텍스트가 가지고 있는 빈을 DI 받을 수 있다면 굳이 getBean()을 이용하지 않고 UserDao 빈을 받을 수 있다.

DI와 테스트

항상 SimpleDriverDataSource를 쓸 예정인데 굳이 Datasource 인터페이스를 사용하고 DI를 통해 주입받는 방식을 쓸 필요가 있을까?

그래도 인터페이스를 두고 DI를 적용해야 한다.
1. 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
2. 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
- 1장에서 만든 DB 커넥션 개수를 카운팅 하는 부가기능이 그런 예다.
3. 테스트 때문이다.
- DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.

테스트 코드에 의한 DI

DI를 이용해 테스트 중 DAO가 사용하는 DataSource 구현 클래스를 바꿔주는 방법으로 운영 DB와 테스트 DB를 분리할 수 있다.

@DirtiesContext
public class UserDaoTest {
    @Autowired
    UserDao userDao;
    
    @Before
    public void setUp() {
        ...
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql://localhost/testdb", "spring", "book", true
        );
        userDao.setDataSource(dataSource);
    }
}
  • 이 방법으로 XML 설정파일 변경없이 테스트 코드를 통해 오브젝트 관계를 재구성 할 수 있다. 하지만 이미 애플리케이션에서 applicationContext.xml 파일의 설정정보에 따라 구성한 오브젝트를 강제로 변경했기에 다른 테스트에서도 변경된 애플리케이션 컨텍스트를 사용하게 된다. 이는 별로 바람직하지 않다. 그래서 @DirtiesContext 어노테이션을 추가해 변경된 컨텍스트를 공유하지 않도록 할 수 있다.
  • @DirtiesContext: 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 에플리케이션 컨텍스트의 상태를 변경한다는 것을 알려주고 애플리케이션 컨텍스트의 공유를 허용하지 않는다.

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

테스트 코드에서 빈 오브젝트를 수동으로 DI 하는 방법은 장점보다 단점이 많다. 코드가 많아져 번거롭기도 하고 애플리케이션 컨텍스트를 매번 만들어야 하는 부담이 있다.


이 문제는 applicationContext.xml 을 복사해 text-applicationContext.xml 을 만들어 운영으로 사용할 DataSource 와 테스트에 적합하게 준비된 DB를 사용하는 가벼운 DataSource 를 빈으로 등록하여 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해줌으로써 해결할 수 있다.

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

이로써 번거롭게 수동 DI 하는 코드나 @DirtiesContext 가 필요 없어졌다.

컨테이너 없는 DI 테스트

  • UserDao 나 DataSource 구현 클래스 어디에도 스프링 API를 직접 사용하거나 애플리케이션 컨텍스트를 이용하는 코드가 존재하지 않는다. 즉, 스프링 DI 컨테이너에 의존하지 않는다.
  • 스프링 컨테이너에서 UserDao가 잘 동작하는지는 UserDaoTest의 관심사가 아니다.
public class UserDaoTest {
    UserDao userDao;
    
    @Before
    public void setUp() {
        ...
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql://localhost/testdb", "spring", "book", true
        );
        userDao.setDataSource(dataSource);
    }
}

DataSource 를 직접 만들어야 하지만 애플리케이션 컨텍스트를 아예 사용하지 않으니 코드는 더 단순해지고 이해하기 쉬워졌다. 그리고 테스트 시간도 짧아졌다.

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

  • 항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다
  • 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우가 있다. 이때는 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다.
  • 테스트에서 애플리케이션 컨텍스트를 사용하는 경우에는 테스트 전용 설정파일을 따로 만들어 사용 하는 편이 좋다. 보통 개발환경과 테스트환경, 운영환경이 차이가 있기 때문에 각각 다른 설정파일을 만들어 사용하는 경우가 일반적이다.
  • 예외적인 의존관계를 강제로 구성 해서 테스트해야 할 경우가 있다. 이때는 컨텍스트에서 DI 받은 오브젝트에 다시 테스트 코드로 수동 DI 해서 테스트하는 방법을 사용하면 된다. 테스트 메소드나 클래스에 @DirtiesContext 를 붙이는 걸 잊지 말자!

2.5 학습 테스트로 배우는 스프링

학습테스트란 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대한 테스트이다.

목적

  1. 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히려는 것
  2. 자신이 테스트를 만들려고 하는 기술이나 기능에 대해 얼마나 제대로 이해하고 있는지, 그 사용 방법을 바로 알고 있는지를 검증
  3. 테스트 코드를 작성하면서 빠르고 정확하게 사용법을 익히는 것

장점

  1. 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
  2. 학습 테스트 코드를 개발 중에 참고할 수 있다.
  3. 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
  4. 테스트 작성에 좋은 훈련이 된다.
  5. 새로운 기술을 공부하는 과정이 즐거워진다.

예시 (JUnit5, AssertJ 활용)

테스트 메소드에서 매번 동일한 application context 가 주입 됐는지 확인하는 테스트

@SpringBootTest
public class JUnitTest {
    @Autowired
    ApplicationContext applicationContext;

    static Set<JUnitTest> testObjects = new HashSet<>();
    static ApplicationContext contextObject = null;

    @Test
    void test1() {
        assertThat(testObjects).doesNotContain(this);
        testObjects.add(this);

        assertThat(contextObject == null || contextObject == this.applicationContext).isTrue();
        contextObject = this.applicationContext;
    }

    @Test
    void test2() {
        assertThat(testObjects).doesNotContain(this);
        testObjects.add(this);

        assertTrue(contextObject == null || contextObject == this.applicationContext);
        contextObject = this.applicationContext;
    }

    @Test
    void test3() {
        assertThat(testObjects).doesNotContain(this);
        testObjects.add(this);

        MatcherAssert.assertThat(contextObject, either(is(nullValue())).or(is(this.applicationContext)));
        contextObject = this.applicationContext;
    }
}

버그 테스트

코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트, 일단 버그가 발생하는 조건의 테스트를 만들고 테스트가 성공하면 버그가 해결되게 한다.

장점

  1. 테스트의 완성도를 높여준다. 기존에 미처 검증하지 못한 부분을 매꾼다
  2. 버그의 내용을 명확하게 분석하게 해준다
  3. 기술적인 문제를 해결하는 데 도움이 된다

동등분할(equivalence partitioning)

같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트하는 방법을 말한다. 어떤 작업의 결과의 종류가 true, false 또는 예외발생 세 가지라면 각 결과를 내는 입력 값이나 상황의 조합을 만들어 모든 경우에 대한 테스트를 해보는 것이 좋다.

경계값 분석(boundary value analysis)

에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다. 보통 숫자의 입력 값인 경우 0이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트해보면 도움이 될 때가 많다.

0개의 댓글