SPRING

토비의 스프링 2. 테스트

짜비 2022. 5. 27. 22:01

작은 단위의 테스트가 필요한 이유

  • 한꺼번에 너무 많은 것을 몰아서 테스트하면 테스트 수행 과정도 복잡해지고, 오류가 발생했을 때 정확한 원인을 찾기가 힘들어진다. (e.g. 웹화면을 이용한 테스트) 따라서 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.
  • 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위함. 다른 사람에 의해 테스트될 때보다 빠르게 오류 수정 가능.

UserDaoTest 특징

  • 자동 수행 테스트 코드
    • 테스트할 데이터가 코드를 통해 제공되고, 테스트 작업도 코드를 통해 자동으로 실행됨. 개발자는 main 메소드를 실행하기만 하면 됨
  • 점진적인 개발을 위한 테스트
    • 기능 추가, 코드 개선할 때 이전 기능이 영향받지 않는지 확인 가능

UserDaoTest 문제점

  • 수동 확인 작업의 번거로움
    • 테스트 output - 기대 output 비교를 사람이 직접 해야 함
  • 실행 작업의 번거로움
    • main() 메소드를 일일이 실행시켜야 함

문제점 해결 방안

  • 테스트 검증의 자동화
if (!user.getName().equals(user2.getName())) {
    System.out.println("테스트 실패");
} else if (!user.getPassword().equals(user2.getPassWord())) {
    System.out.println("테스트 실패");
} else {
    System.out.println("테스트 성공");
} 
  • 테스트 효율적인 수행과 결과 관리 : JUnit 테스트로 전환
public class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        UserDao userDao = context.getBean("userDao", UserDao.class);
        User user = new User();
        user.setId("abcd");
        user.setName("sb");
        user.setPassword("123");

        userDao.add(user);

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

        Assert.assertThat(user2.getName(), CoreMatchers.is(user.getName()));

    }
}

테스트 결과의 일관성

  • 단위테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다.
  • UserDaoTest 실행 전에 DB 의 USER 테이블 데이터를 모두 삭제해줘야 함.
    • 해결책은, 테스트를 마치고 나면 DB 데이터를 삭제해서 테스트를 수행하기 이전 상태로 만드는 것.
public class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        UserDao userDao = context.getBean("userDao", UserDao.class);

        userDao.deleteAll();

        assertThat(userDao.getCount(), is(0));

        User user = new User();
        user.setId("abcd");
        user.setName("sb");
        user.setPassword("123");

        userDao.add(user);
        assertThat(userDao.getCount(), is(1));

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

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

    }
}
  • 더 꼼꼼한 테스트를 해보는 것이 좋은 자세다. 테스트를 안 만드는 것도 위험한 일이지만, 성의 없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는 건 더 위험하다. 특히 한 가지 결과만 검증하고 마는 것은 상당히 위험하다. 이런 테스트는 마치 하루에 두 번은 정확히 맞는다는 시계와 같을 수도 있다. 죽은 시계 말이다.

예외 조건에 대한 테스트

@Test(expected = EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException, ClassNotFoundException {
    ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
    UserDao userDao = context.getBean("userDao", UserDao.class);

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

    userDao.get("unknown");    //예외 발생
}
  • EmptyResultDataAccessException 예외처리를 하지 않았기 때문에 위 테스트는 실패한다. 테스트가 통과하도록 get() 을 수정해보자.
public User get(String id) throws EmptyResultDataAccessException, SQLException {
    Connection connection = getConnection();

    PreparedStatement ps = connection.prepareStatement(
            "select * from users where id = ?"
    );
    ps.setString(1, id);



    ResultSet rs = ps.executeQuery();

    User user = null;
    if (rs.next()) {
        user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
    }

    rs.close();
    ps.close();
    connection.close();

    if (user == null) {
        throw new EmptyResultDataAccessException();
    }

    return user;
}

테스트 코드 작성시 주의사항

  • 개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있다. 바로 성공하는 케이스만 골라서 만드는 것이다.
  • 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다. “항상 네거티브 테스트를 먼저 만들라”
  • 코드를 만들고 나서 시간이 많이 지나면 테스트를 만들기가 귀찮아진다. … 결국 테스트 작성은 자꾸 뒷전으로 밀려나거나 점점 더 성의 없는 테스트를 만들게 될지도 모른다.

TDD 장점

  • 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
  • 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧다. 개발한 코드의 오류는 빨리 발견할수록 좋다. … 테스트 없이 오랜 시간 동안 코드를 만들고 나서 테스트를 하면, 오류가 발생했을 때 원인을 찾기가 쉽지 않다.

테스트 코드 개선

  • 중복 제거
public class UserDaoTest {

    private UserDao userDao;

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

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
        userDao.deleteAll();

        assertThat(userDao.getCount(), is(0));

        User user = new User();
        user.setId("abcd");
        user.setName("sb");
        user.setPassword("123");

        userDao.add(user);
        assertThat(userDao.getCount(), is(1));

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

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

    }

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

        userDao.get("unknown");    //예외 발생
    }

}

JUnit 이 테스트를 수행하는 과정

  • 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다.
    • why? : 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장하기 위해서.

테스트를 위한 applicationContext 관리

  • 이전 코드의 문제점 : 테스트 메소드를 수행할 때마다 ApplicationContext를 새로 생성 -> 시간이 오래 걸리며, 비효율적.
  • 문제 해결
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {

    @Autowired
    private ApplicationContext context;

    private UserDao userDao;

    @BeforeEach
    void setUp() {
        this.userDao = this.context.getBean("userDao", UserDao.class);
    }

    @Test
    void addAndGet() throws SQLException, ClassNotFoundException {
        userDao.deleteAll();

        assertThat(userDao.getCount(), is(0));

        User user = new User();
        user.setId("abcd");
        user.setName("sb");
        user.setPassword("123");

        userDao.add(user);
        assertThat(userDao.getCount(), is(1));

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

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

    }

    @Test
    public void getUserFailure() throws SQLException, ClassNotFoundException {
        userDao.deleteAll();
        assertThat(userDao.getCount(), is(0));

        userDao.get("unknown");    //예외 발생
    }

}
  • JUnit 확장 기능 동작 방식
    • 테스트가 실행되기 전에 딱 한 번만 ApplicationContext를 만들어두고, 테스트 오브젝트가 만들어 질때마다 특별한 방법을 이용해 ApplicationContext 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것.

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

  • 여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 ApplicationContext를 사용한다면, 스프링은 서로 다른 테스트 클래스 사이에서도 ApplicationContext를 공유하게 해준다.

테스트에 DI를 이용하는 방법

테스트 코드에 의한 DI

  • 테스트 코드 에서 Setter를 이용해서 DI 를 해준다.
    • e.g. dataSource가 테스트용 db로 연결되도록 변경.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/applicationContext.xml")
@DirtiesContext    //테스트 메소드에서 AC의 구성이나 상태를 변경한다는 것을 test context 프레임워크에 알려준다.
public class UserDaoTest2 {
    @Autowired
    UserDao dao;

    @BeforeEach
    void setUp() {
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql://localhost/testdb", "spring", "book", true);
        dao.setDataSource(dataSource);
    }
}

  • 주의사항 : 빈의 의존관계를 강제로 변경. -> 다른 테스트를 수행할 때 변경된 AC가 계속 사용될 것. -> 바람직하지 않다.
  • 해결 : @DirtiesContext ApplicationContext를 공유하지 않음. 즉, 테스트 메소드가 수행되고 나면 매번 새로운 AC를 생성.

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

  • 테스트에서 사용될 DataSource 만을 위한 설정파일을 따로 만드는 방법.
  • 설정파일을 따로 만들고, 설정파일 경로만 수정해주면 된다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserDaoTest2 {
  //...
}

컨테이너 없는 DI 테스트. ( Q. 생성자를 활용한 방식?)

  • UserDaoTest 는 스프링 DI 컨테이너에 의존하지 않는다. 즉, 스프링 API를 직접 사용하거나 AC를 이용하는 코드가 없다.
    • 따라서, 테스트 코드에서 직접 오브젝트를 만들고 DI를 해줄 수 있다.
public class UserDaoTest2 {
    UserDao dao;

    @BeforeEach
    void setUp() {
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql://localhost/testdb", "spring", "book", true);
        dao.setDataSource(dataSource);
    }
}
  • 장점
    • AC를 이용하지 않아 코드가 단순하고 이해하기 편하다
    • 테스트 실행 시간이 짧다.

학습테스트

  • 자신이 사용할 API 나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 것
  • 장점
    • 다양한 조건에 따른 기능을 손쉽게 확인 가능
    • 학습테스트 코드를 추후 개발 할 때 참고 가능. 테스트 작성 훈련
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class JUnitTest {
  //ApplicationContext를 하나만 생성해서 공유하는지 테스트

    @Autowired
    ApplicationContext context;

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

    @Test
    void test1() {
        assertThat(testObjects, not(hasItem(this)));
        testObjects.add(this);

        assertThat(contextObject == null || contextObject == this.context, is(true));
        contextObject = this.context;
    }

    @Test
    void test2() {
        assertThat(testObjects, not(hasItem(this)));
        testObjects.add(this);

        Assertions.assertTrue(contextObject == null || contextObject == this.context);
        contextObject = this.context;
    }

    @Test
    void test3() {
        assertThat(testObjects, not(hasItem(this)));
        testObjects.add(this);

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


버그테스트

  • 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트
  • 버그가 발견되었을 때 무턱대고 코드를 뒤져가면서 수정하려고 하기보다는 먼저 버그 테스트를 만들어보는 편이 유용하다.