토비의 스프링을 학습하며 정리한 내용이며 관련 코드는 여기서 확인하실 수 있습니다.

애플리케이션은 계속 변하고 복잡해져 간다. 그 변화에 대응하는 첫 번째 전략이 확장 과 변화를 고려한 객체지향적 설계와 그것을 효과적으로 담아낼 수 있는 IoC/DI 같은 기술이라면, 두 번째 전략은 만들어진 코드를 확신할 수 있게 해주고, 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술이다.

테스트란?

테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 단순 무식한 방법으로 정상동작하는 코드를 만들고, 테스트를 만들어 뒀기 때문에 매우 작은 단계를 거쳐가면서 계속 코드를 개선할 수 있었던 것도 결국 테스트가 존재했기에 가능한 것이다.

  • 단위 테스트
  • 자동화된 테스트
  • 통합 테스트
  • 학습 테스트
  • 버그 테스트 등등

테스트 리팩토링

  • 현재의 UserDaoTest
    • 수동 확인 작업의 번거로움 - 자동화 필요
    • 실행 작업의 번거로움 - 다른 테스트가 추가되면 여러 메인 메소드를 실행해야 한다. - 자동화 필요
    • 직접 제어권을 가진다 - IoC의 기능을 활용하지 못하고 있음.
    • main method 테스트
    public class UserDaoTest {
        public static void main(String[] args) throws SQLException, ClassNotFoundException {
            UserDao dao = new DaoFactory().userDao();

            dao.deleteAll();

            User user = new User();
            user.setId("kyle");
            user.setName("siyoung");
            user.setPassword("kim");

            dao.add(user);

            System.out.println(user.getId() + "등록 성공");

            User user2 = dao.get(user.getId());
            System.out.println(user2.getName());
            System.out.println(user2.getPassword());

            System.out.println(user2.getId() + "조회 성공");
        }
    }
  • 개선된 UserDao
    • 자동화된 TEST로 인해 기존의 테스트의 문제점 해결
    • 제어권을 스프링에게 넘김으로써 프레임워크 기반으로 프로그래밍하라 가능
    @SpringBootTest
    class UserDaoTest {
        @Autowired
        private ApplicationContext context;

        private UserDao dao;

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

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

            this.user1 = new User("AAA", "BBB", "CCC");
            this.user2 = new User("BBB", "DDD", "DDD");
            this.user3 = new User("CCC", "EEE", "EEE");
        }

        @Test
        void addAndGet() throws SQLException, ClassNotFoundException {
            dao.deleteAll();
            assertThat(dao.count()).isEqualTo(0);

            dao.add(user1);
            dao.add(user2);
            assertThat(dao.count()).isEqualTo(2);

            User findUser1 = dao.get(user1.getId());
            assertThat(findUser1).isEqualToComparingFieldByField(user1);

            User findUser2 = dao.get(user2.getId());
            assertThat(findUser2).isEqualToComparingFieldByField(user2);
        }

        @Test
        void getUserFail() throws SQLException, ClassNotFoundException {
            dao.deleteAll();
            assertThat(dao.count()).isEqualTo(0);

            assertThatThrownBy(() -> dao.get("NOT_EXIST"))
                .isInstanceOf(Exception.class);
        }

        @Test
        void count() throws SQLException, ClassNotFoundException {
            dao.deleteAll();
            assertThat(dao.count()).isEqualTo(0);

            dao.add(user1);
            assertThat(dao.count()).isEqualTo(1);

            dao.add(user2);
            assertThat(dao.count()).isEqualTo(2);

            dao.add(user3);
            assertThat(dao.count()).isEqualTo(3);
        }
    }

TDD

추가하고 싶은 기능을 코드로 표현한다면 이는 하나의 기능 명세서가 된다. 실제로 테스트는 조건, 행위, 결과로 이루어져 있는데 이는 마치 하나의 기능 명세서와 같다. 이러한 기능 명세서를 코드로써(테스트) 우선적으로 작성하고 이를 검증하기 위한 Production코드를 작성하는 것을 TDD 방법론이라고 말한다.

개발자들이 정신없이 개발을 하다 보면 사이사이 테스트를 만들어서 코드를 점검할 타이밍을 놓치는 경우가 많다. 문제는 코드를 만들고 나서 시간이 많이 지나면 테스트를 만들기가 귀찮아진다는 점이다. 또, 작성한 코드가 많기 때문에 무 엇을 테스트해야 할지 막막할 수도 있다. 결국 테스트 작성은 자꾸 뒷전으로 밀려나거나 점점더성의없는테스트를만들게될지도모른다.

Junit의 동작 방식

JUnit 프레임워크가 테스트 메소드를 실행 하는 과정을 알아야 한다. 프레임워크는 스스로 제어권을 가지고 주도적으로 동작하고, 개발자가 만든 코드는 프레임워크에 의해 수동적으로 실행한다. 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실 히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다. 덕분에 인스턴스 변수도 부담 없이 사용할 수 있다. 어차피 다음 테스트 메소드가 실행될 때는 새로운 오브젝트가 만들 어져서 다 초기화될 것이다.

JUnit 확장기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들 어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스 트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다. 테스트 클래스 사이에서도 어플리케이션 컨텍스트는 공유되어 함께 사용된다.

  1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다.
  3. @Before가 붙은 메소드가 있으면
  4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를
  5. @After가 붙은 메소드가 있으면
  6. 나머지 테스트 메소드에 대해 2~5번을
  7. 모든 테스트의 결과를 종합해서 돌려준다.

DI를 통한 테스트 선택 방법

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

주의점

  1. 단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다는 점을 잊어선 안 된다. DB에 남아 있는 데이터와 같은 외부 환경에 영향을 받지 말아야 하는 것은 물론이고, 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 한다.
  2. 테스트를 안 만드는 것도 위험한 일이지만, 성의 없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는 건 더 위험하다. 특히 한 가지 결과만 검증하고 마는 것은 상당히 위험하다. 이런 테스트는 마치 하루에 두 번 은정확히맞는다는시계와같을수도있다.죽은시계말이다.
  3. 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다. get() 메소드의 경우라면, 존재하는 id가 주어졌을 때 해당 레코드를 정확히 가져오는가 를 테스트하는 것도 중요하지만, 존재하지 않는 id가 주어졌을 때는 어떻게 반응할지를 먼저 결정하고, 이를 확인할 수 있는 테스트를 먼저 만들려고 한다면 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발이 가능하다.

정리

  • 테스트는 자동화돼야 하고, 빠르게 실행할 수 있어야 한다.

  • main() 테스트 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리하다.

  • 테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서
    결과가 달라지면 안 된다.

  • 테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것보다 나쁠 수
    있다.

  • 코드 작성과 테스트 수행의 간격이 짧을수록 효과적이다.

  • 테스트하기 쉬운 코드가 좋은 코드다.

  • 테스트를 먼저 만들고 테스트를 성공시키는 코드를 만들어가는 테스트 주도 개발 방법도 유
    용하다.

  • 테스트 코드도 애플리케이션 코드와 마찬가지로 적절한 리팩토링이 필요하다.

  • @Before, @After를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수

    있다.

  • 스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상시킬 수 있다.

  • 동일한 설정파일을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 공유한다.

  • @Autowired를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI 할 수 있다.

  • 기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하자.

  • 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용하다.

0개의 댓글