티스토리 뷰

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;
    }
}


버그테스트

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

'SPRING' 카테고리의 다른 글

토비의 스프링 4. 예외  (0) 2022.05.27
토비의 스프링 3. 템플릿  (0) 2022.05.27
토비의 스프링 1. 오브젝트와 의존관계  (0) 2022.05.27
request 스코프(feat. Provider, Proxy)  (0) 2021.05.21
프로토타입 스코프  (0) 2021.05.20
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함