JUnit @Before(@After)사용으로 테스트 코드 중복 제거 및 스프링 테스트 적용

2022. 1. 16. 16:44북리뷰/토비의 봄

728x90

이전 포스팅에 작성했던 테스트 코드를 살펴보자

public class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);

        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        User user = new User();
        user.setId("dkwip");
        user.setName("test");
        user.setPassword("1234");

        dao.add(user);

        User user2 = dao.get(user.getId());

        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }

    @Test
    public void count() throws SQLException {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(DaoFactory.class);

        UserDao dao = context.getBean("userDao", UserDao.class);
        User user1 = new User("123","1234","132");
        User user2 = new User("1243","12344","13342");
        User user3 = new User("13243","12344","13342");

        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.add(user1);
        assertThat(dao.getCount(), is(1));

        dao.add(user2);
        assertThat(dao.getCount(), is(2));

        dao.add(user3);
        assertThat(dao.getCount(), is(3));
    }

    @Test(expected = EmptyResultDataAccessException.class)
    public void userNotFound() throws SQLException {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(DaoFactory.class);

        UserDao dao = context.getBean("userDao", UserDao.class);
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.get("uuuuuu");
    }
}

위 코드를 살펴보면 반복되는 부분이 눈에 띈다. 다름과 같이 스프링의 애플리케이션 컨텍스트를 만드는 부분과 컨텍스트에서 UserDao를 가져오는 부분이다.

ApplicationContext context =
                new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);

중복된 코드는 별도의 메서드로 뽑아내는 것이 가장 손쉬운 방법이다. 허나, JUnit은 테스트 메서드를 실행할 때 부가적으로 해주는 작업이 몇가지 있다. 그중에서 테스트를 실행할 때마다 반복되는 준비 작업을 별도의 메서드에 넣게 해주고, 이를 매번 테스트 메서드를 실행하기 전에 먼저 실행해주는 기능이다.

@Before

중복됐던 코드를 넣을 setUp이라는 메서드를 생성하고, 중복되는 코드를 추출한다. 그리고 setUp()에는 @Before 메서드를 추가해준다. 다음은 수정한 코드다.

public class UserDaoTest {

    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setUp() {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(Config.class);
        this.dao = context.getBean("userDao", UserDao.class);

        user1 = new User("123","1234","132");
        user2 = new User("1243","12344","13342");
        user3 = new User("13243","12344","13342");
    }

    @Test
    public void addAndGet() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        User user = new User();
        user.setId("dkwip");
        user.setName("test");
        user.setPassword("1234");

        dao.add(user);

        User user2 = dao.get(user.getId());

        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }

    @Test
    public void count() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.add(user1);
        assertThat(dao.getCount(), is(1));

        dao.add(user2);
        assertThat(dao.getCount(), is(2));

        dao.add(user3);
        assertThat(dao.getCount(), is(3));
    }

    @Test(expected = EmptyResultDataAccessException.class)
    public void userNotFound() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.get("uuuuuu");
    }
}

수정 뒤에 테스트를 진행해보면 정상적으로 실행이 될 것이다. JUnit이 테스트를 수행하는 방식은 다음과 같다.

  1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메서드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 생성한다.
  3. @Before가 붙은 메서드가 있으면 실행한다.
  4. @Test가 붙은 메서드를 하나 호출하고 테스트 결과를 저장해둔다.
  5. @After가 붙은 메서드가 있으면 실행한다.
  6. 나머지 테스트 메서드에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해서 돌려준다.
    이처럼, JUnit은 @Test가 붙은 메서드를 실행하기 전과 후에 각각 @Before와 @After가 붙은 메서드를 자동으로 실행한다. 보통 테스트 메서드 실행 전에 공통적인 준비작업과 정리작업이 필요한 경우가 많은데, @Before과 @After로 매우 편리하게 처리할 수 있다.

스프링 테스트 적용

위 코드를 보면 @Before 메서드가 테서트 메서드 개수만큼 반복되기 때문에 애플리케이션 컨텍스트도 세 번 만들어진다. 위 예시는 간단한 예시여서 별 문제가 아닌 듯 보이지만, 빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 걸릴 수 있다. 애플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화한다. 또한, 테스트를 마칠 때마다 애플리케이션 컨텍스트 내의 빈이 할당한 리소스 등을 깔끔하게 정리해주지 않으면 다음 테스트에서 새로운 애플리케이션 컨텍스트가 만들어지면서 문제를 일으킬 수도 있다.
스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 테스트 컨텍스트의 지원을 받으면 간단한 애너테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.
적용을 위해 gradle.build에 dependency를 추가한다.

dependencies {
   ...
    testImplementation 'org.springframework:spring-test:5.2.6.RELEASE'
}

다음은 스프링 테스트 컨텍스트를 적용한 예이다

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Config.class)
public class UserDaoTest {

    @Autowired
    private ApplicationContext context;

    private UserDao dao;

    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setUp() {
        this.dao = this.context.getBean("userDao", UserDao.class);

        user1 = new User("123","1234","132");
        user2 = new User("1243","12344","13342");
        user3 = new User("13243","12344","13342");
    }

    @Test
    public void addAndGet() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        User user = new User();
        user.setId("dkwip");
        user.setName("test");
        user.setPassword("1234");

        dao.add(user);

        User user2 = dao.get(user.getId());

        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }

    @Test
    public void count() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.add(user1);
        assertThat(dao.getCount(), is(1));

        dao.add(user2);
        assertThat(dao.getCount(), is(2));

        dao.add(user3);
        assertThat(dao.getCount(), is(3));
    }

    @Test(expected = EmptyResultDataAccessException.class)
    public void userNotFound() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.get("uuuuuu");
    }
}

애플리케이션 컨텍스트는 @Autowired로 스프링 컨테이너에서 자동으로 주입했다. @Autowired는 아래에 설명하겠다.
@RunWith은 JUnit의 테스트 실행 방법을 확장할 때 사용하는 애너테이션이다. SpringJUnit4ClassRunner를 지정하여 확장한 것이다.
@ContextConfiguration은 자동으로 생성한 애플리케이션 컨텍스트의 설정파일을 지정한 것이다.
참고로, 테스트를 진행할 때 마다 스프링 컨테이너에서 context를 새로 생성한다.

@Autowired

Bean 주입 시 스프링 컨테이너에서 자동으로 주입하는 객체들에게 붙인다. @Autowired가 붙은 인스턴스 변수가 있으면, 컨텍스트 프레임워크는 변수 탑입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 일반적으로는 주입을 위해 생성자나 수정자 메서드를 사용한다. 또한 별도의 DI설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있다. 위 경우에서 UserDao도 스프링 컨테이너 내에 빈으로 생성되어 있으므로 @Autowired 주입이 가능하다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Config.class)
public class UserDaoTest {

    @Autowired
    private ApplicationContext context;

    @Autowired
    private UserDao dao;

    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setUp() {
        //this.dao = this.context.getBean("userDao", UserDao.class); 직접 찾을 필요 없음! @Autowired가 자동 주입

        user1 = new User("123","1234","132");
        user2 = new User("1243","12344","13342");
        user3 = new User("13243","12344","13342");
    }

    @Test
    public void addAndGet() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        User user = new User();
        user.setId("dkwip");
        user.setName("test");
        user.setPassword("1234");

        dao.add(user);

        User user2 = dao.get(user.getId());

        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }

    @Test
    public void count() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.add(user1);
        assertThat(dao.getCount(), is(1));

        dao.add(user2);
        assertThat(dao.getCount(), is(2));

        dao.add(user3);
        assertThat(dao.getCount(), is(3));
    }

    @Test(expected = EmptyResultDataAccessException.class)
    public void userNotFound() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.get("uuuuuu");
    }
}

테스트시 컨텍스트 수동 DI 적용

만약 현재 Config에서 설정된 Datasource가 실제 운영에 사용하는 DB와 연결된 것이면 어떨까? 만약 테스트 중 deleteAll()을 호출하면 대참사가 날 것이다. 그렇다고 코드를 직접 수정해서 연결된 DB location을 변경하고 싶지는 않다. 그러면 어떻게 해야 테스트용 DB 서버와 연결을 할 수 있을까?

@DirtiesContext 애너테이션을 사용하면 된다. 이 에너테이션은 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다. 테스트 컨텍스트는 이 에너테이션이 붙은 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 테스트 메서드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어 다음 테스트가 사용하게 해준다. 이렇게 하면 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게 된다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Config.class)
@DirtiesContext
public class UserDaoTest {

    @Autowired
    private ApplicationContext context;

    @Autowired
    private UserDao dao;

    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setUp() {
        DataSource dataSource = new SingleConnectionDataSource(
                "Jdbc:mysql://localhost/testdb", "root", "<password>", true);
        dao.setDataSource(dataSource);
        user1 = new User("123","1234","132");
        user2 = new User("1243","12344","13342");
        user3 = new User("13243","12344","13342");
    }

    @Test
    public void addAndGet() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        User user = new User();
        user.setId("dkwip");
        user.setName("test");
        user.setPassword("1234");

        dao.add(user);

        User user2 = dao.get(user.getId());

        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }

    @Test
    public void count() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.add(user1);
        assertThat(dao.getCount(), is(1));

        dao.add(user2);
        assertThat(dao.getCount(), is(2));

        dao.add(user3);
        assertThat(dao.getCount(), is(3));
    }

    @Test(expected = EmptyResultDataAccessException.class)
    public void userNotFound() throws SQLException {
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));

        dao.get("uuuuuu");
    }
}
728x90