2장 테스트

@MinSang · August 24, 2024 · 14 min read

테스트란

테스트란, 본인이 예상하고 의도했던 대로 코드가 정확히 동작하는 지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업입니다.

또한, 테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있다는것을 알 수 있습니다. 이를 통해 코드의 결함을 제거해가는 작업, 일명 디버깅을 거치게 되고, 결국 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있습니다.

작은 단위의 테스트

하나의 관심에 집중할 수 있게 작은 단위로 만들어진 테스트 입니다.

테스트 중에 DB가 사용되면 단위 테스트일까 아닐까

테스트가 DB의 상태를 관장하고 있다면 이는 단위 테스트라고 해도 됩니다. 다만, DB의 상태가 매번 달라지고, 테스트를 위해 DB를 특정 상태로 만들어줄 수 없다면 그때는 단위테스트로서 가치가 없어집니다.

정리하면, 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 볼 수 있습니다.

단위테스트가 필요한 이유

개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는 지를 개발자 스스로 빨리 확인받기 위해서입니다.

유의할 점

단위 테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 합니다. 즉, 항상 일관성 있는 결과가 보장돼야 하며 DB에 남아 있는 데이터와 같은 외부 환경에 영향을 받지 말아야 하고, 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 합니다.

자동화된 테스트

테스트 자체가 사람의 수작업을 거치는 방법을 사용하기보다는 코드로 만들어져서 자동으로 수행될 수 있어야 합니다.

간단한 수정을 빨리 테스트로 확인해야 하는데 굉장히 귀찮은 작업이 되면 안되기 때문입니다.

JUnit 프레임워크

Junit은 프레임워크입니다. 프레임워크의 기본 동작원리가 제어의 역전(IoC)입니다. 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어합니다.

개발자가 만든 클래스의 오브젝트를 생성하고 실행하는 일은 프레임워크에 의해 진행됩니다. 따라서 프레임워크에서 동작하는 코드는 main 메소드도 필요없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없습니다.

TDD (테스트 주도 개발)

만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고나서 , 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발방법입니다.

장점은 시간이 진면서 테스트를 빼먹지 않고 꼼꼼하게 만들 수 있다는 점이 있습니다. 또, 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧기 때문에 오류를 일찍 발견하고, 쉽게 대응이 가능합니다.

주의점은, TDD에서는 테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 한 짧게 가져가도록 권장합니다. 테스트를 반나절 동안 만들고 오후 내내 테스트를 통과시키는 코드를 만드는 식의 개발은 좋은 방법이 아닙니다.

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

  • 테스트 클래스에서 @Test가 붙은 void형이며 파라미터가 없는 테스트 메소드를 모두 찾습니다.
  • 테스트 클래스의 오브젝트를 하나 만듭니다.
  • @BeforeEach가 붙은 메소드가 있으면 실행합니다.
  • @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둡니다.
  • @AfterEach가 붙은 메소드가 있으면 실행합니다.
  • 나머지 테스트 메소드에 대해 2~5번을 반복합니다.
  • 모든 테스트의 결과를 종합해서 돌려줍니다.

정리하면, 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만듭니다.

Tip

  1. 테스트 메소드 일부에서만 공통적으로 사용되는 코드가 있다면 @BeforeEach를 사용하기 보다는,

    일반적인 메소드 추출 방법을 써서 메소드를 분리하고 테스트 메소드에서 직접 호출해 사용하도록 만들거나 아예 공통적인 특징을 지닌 테스트 메소드를 모아서 테스트 클래스를 만드는 방법이 있습니다.

  2. @BeforeEach 메소드의 정보를 테스트 메소드에서도 써야 한다면 인스턴스 변수를 활용하면 됩니다.

테스트 컨텍스트 프레임워크

JUnit5에서는 다음과 구성합니다.

@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/applicationContext.xml")
public class UserDaoTest {
    @Autowired ApplicationContext applicationContext;
    UserDao userDao;

    @BeforeEach
    public void setUp() {
        System.out.println("applicationContext = " + applicationContext);
        System.out.println("this = " + this);
        this.userDao = this.applicationContext.getBean("userDao", UserDao.class);
        ...
    }
    ...
}
출처: https://jake-seo-dev.tistory.com/19 [제이크서 위키 블로그:티스토리]

@ExtendWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션입니다. SpringExtesion을 지정해주면 JUnit용 애플리케이션 컨텍스트를 만들고 Spring에서 제공하는 테스트 지원 기능들을 사용할 수 있습니다.

@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정 파일 위치를 지정합니다.

이렇게 생성한 테스트 애플리케이션 컨텍스트는 모든 테스트 메소드가 공유할 수 있습니다. 매번 테스트 메소드마다 애플리케이션을 생성하는 것은 매우 많은 시간이 걸리기 때문에 이와같은 어노테이션을 활용합니다.

또한, 여러 개의 테스트 클래스가 있는 데 모두 같은 설정파일을 가진 애플리케이션 컨택스트를 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해줍니다.

@Autowired

@Autowired는 스프링의 DI에 사용되는 특별한 어노테이션입니다.

@Autowired가 붙은 인스턴스 변수가 있다면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾습니다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입합니다.

만약, 타입으로 가져올 빈이 여러개인 경우에는 변수의 이름과 같은 이름의 빈이 있는 지 확인하고 같은 이름의 빈을 주입합니다. 만약에 변수 이름으로도 빈을 갖고 올수 없는 경우엔 예외가 발생합니다.

추가적으로, 애플리케이션 컨택스트는 자기 자신도 빈으로 등록합니다. 그래서 위 코드처럼 DI가 가능합니다.

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

테스트에서만 사용할 빈이 따로 있을 수 있습니다. 예를들면 DataSource의 빈을 생성할 때 운영쪽에서와 테스트에서 사용하는 db가 분리되어야 합니다.

수동으로 DI를 하는 방법도 있지만 (@DiretiesContext 사용) 이는 매번 메소드마다 새로운 애플리케이션 컨텍스트를 띄어야 하기 때문에 좋지 않습니다.

다른 방법으로 테스트 전용 설정파일을 따로 만드는 방법이 있습니다. 즉, 두 가지 종류의 설정파일을 만들어서 하나에는 서버에서 운영용으로 사용할 DataSource를 빈으로 등록해두고, 다른 하나에는 테스트에 적합하게 준비된 DB를 사용하는 가벼운 DataSource가 빈으로 등록하게 만드는 것입니다.

그리고 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해주면 됩니다.

컨테이너 없는 DI 테스트

스프링 컨테이너 없이 테스트 코드의 수동 DI만을 이용하는 방법으로, 테스트 시간도 절약할 수 있습니다.

이는 스프링의 API를 의존하지 않을 떄 가능합니다.

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

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

  • 항상, 스프링 컨테이너 없이 테스트할 수 있는 방법을 고려합니다. 간결하고, 테스트 수행속도가 빠르기 때문입니다.
  • 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트할 경우에는 스프링 DI 방식의 테스트를 이용하면 편리합니다.

정리

  • 테스트는 자동화돼야하고, 빠르게 실행할 수 있어야 합니다.
  • main() 테스트 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리합니다.
  • 테스트 결과는 일관성이 있어야 합니다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안됩니다.
  • 코드 작성과 테스트 수행의 간격은 짧을 수록 효과적 입니다.
  • 테스트하기 쉬운 코드가 좋은 코드입니다. -> (인터페이스를 이용한 DI 적용)
  • TDD는 매우 유용합니다.
  • 테스트 코드도 애플리케이션 코드처럼 적절한 리펙토링이 필요합니다.
  • @BeforeEach, @AfterEach를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있습니다.
  • 스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상 시킬 수 있습니다.
  • 동일한 설정파일을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 공유합니다.
  • @Autowired를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI 할 수 있습니다.
  • 기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하자
  • 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용합니다.

Reference

토비의 스프링 Vol.1

@MinSang
지식과 경험을 기록하는 TIL 저장소