토비의 스프링 3.1 vol.1 2장 - 테스트

2장에선 스프링이 개발자에게 주는 가장 중요한 가치인 객체지향 프로그래밍과 테스트 중 두번째, 테스트에 대해 설명한다.

계속 변하고 복잡해지는 어플리케이션의 변화에 대응하는 방법에는 크게 두가지가 있다.

  1. 확장과 변화를 고려한 객체지향 설계와 IoC/DI
  2. 만들어진 코드에 확신을 주고, 변화에 유연하게 대처하도록 하는 테스트

확장과 변화를 고려한 설계도 중요하지만, 변화된 구현이 이전과 동일한 기능을 수행하는 것을 보장해 주는 것은 테스트 밖에 없다. 일반적으로 main() 메소드 내부에 테스트 코드를 작성하고 이를 실행하여 테스트하는 방법을 통해 코드를 검증 할 수 있다.

웹에서의 테스트

DAO를 테스트할 때 웹을 통해 테스트를 하는 방법이다. 어플레케이션을 실행하기 위해서 DAO 뿐만 아니라 서비스, MVC 프레젠테이션 등 다른 클래스를 모두 구현해야한다. 어플리케이션을 실행하면 테스트의 대상인 DAO 뿐만아니라 다른 구현체들도 테스트 결과에 영향을 줄 수 있다.

단위 테스트

SoC를 통해 테스트에서도 테스트 대상이 되는 단위로 쪼개서 테스트를 할 필요가 있다. 이를 단위 테스트(Unit Test)라고 한다. 단위가 무엇인지 그 범위가 어디까지인지 정해진 건 아니지만 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위의 단위로 생각하면 된다. 일반적으로 단위는 작을수록, 외부의 코드들을 신경쓰지않고, 참여하지 않고 테스트가 동작할 수 있을 수록 좋다.

자동수행 테스트 코드

테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다. 테스트 자체가 결과값을 비교하는 등의 사람의 수작업을 거치는 방법을 사용하기보다는 코드로 만들어져서 자동으로 수행될 수 있어야 한다는 건 매우 중요하다.

어플리케이션을 구성하는 클래스 안에 테스트 코드를 넣는 것 보다 별도의 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫다. 자동으로 수행되는 테스트의 장점은 자주 반복할 수 있다는 것이다.

main() 메소드를 통해 테스트하는 것의 문제점

  • 결과 수동 확인 작업의 번거로움 : 콘솔을 보고 데이터 등록과 조회가 작동하는지 확인하는건 사람의 책임이 된다.
  • 실행 작업의 번거로움 : 테스트 해야할 클래스가 많아질 수록 main() 함수를 실행해야하는 수가 증가한다.

테스트 검증의 자동화

콘솔을 통해 직접 데이터 값을 확인하는 것이 아니라 코드상에서 테스트를 통과했는지 아닌지를 확인하고 테스트에 대한 결과만 콘솔로 띄워주는 작업이다.

하지만 위의 작업으로도 main() 메소드를 일일이 호출해주는 번거로운 작업이 남아있기 때문에, 자바 테스팅 프레임 워크인 JUnit으로 테스트를 전환하면 이를 해결 할 수 있다.

테스트 결과의 일관성

코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야한다. DB등을 사용하는 경우라면 테스트 환경에서 항상 같은 DB 상태를 유지해야한다. 때문에 DB 테이블을 모두 초기화 한 후 테스트를 하는 등의 테스트 전에 환경을 동일하게 하는 선작업이 필요하다.

포괄적인 테스트

테스트는 첫부분과 끝부분에 대해서만 테스트하는 것이 아니라, 이외의 다양한 경우의 수를 생각하며 테스트하는 것이 좋다.

포괄적인 테스트를 위한 조건들

  • 첫부분 테스트
  • 끝부분 테스트
  • 중간부분 테스트
  • 갯수별 테스트
  • 예외조건에 대한 테스트

긍정적인 경우를 골라 성공할 만한 테스트를 먼저 작성하게 되기 쉽다. 항상 네거티브 테스트를 먼저 만드는 습관을 들인다면 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발이 가능하다.

JUnit

테스트 메소드의 제어 권한을 JUnit에게 넘겨주기 위해 테스트 메소드를 public으로 선언하고 @Test 어노테이션을 붙여준다. 또한 테스트의 검증을 JUnit에서 제공하는 assetThat()과 같은 메서드로 대체한다. JUnit의 실행은 main() 메소드안에서 JUnitCore.main() 메소드를 통해 실행한다.

위와같은 방식으로 테스트를 할 때, 테스트의 수가 많아지면 관리가 힘들어지는 단점이 있다. 하지만 IDE에서 제공하는 JUnit 테스트 지원 도구를 통해 이를 해결할 수 있다.

IDE에서 테스트

많은 자바 개발자가 사용하고 있는 IDE인 이클립스나 IntelliJ의 경우 JUnit을 지원하는 기능을 제공한다. IDE의 Run As 항목중 JUnit Test를 선택하면 테스트가 자동으로 실행되게 할 수 있다.

빌드 툴에서 테스트

프로젝트 빌드를 위한 ANT나 메이븐과 같은 빌드 툴들도 JUnit 플러그인이나 태스크를 통해 JUnit 테스트를 실행할 수 있다. 개인의 경우 IDE를 사용하는 방법이 가장 간편하지만, 여러 개발자가 개발의 참여하는 경우 서버에서 모든 코드를 가져와 통합한 뒤에 테스트를 수행하는 것이 좋다.

테스트가 이끄는 개발

추가하고 싶은 기능을 코드로 먼저 표현하고 테스트로 만든 후 기능을 개발하면 테스트가 먼저 선행될 수 있다. 만약 테스트가 실패한다면 이때는 설계한 대로 코드가 작성되지 않았음을 바로 알 수 있다. 그리고 문제가 되는 부분이 무엇인지에 대한 정보도 테스트를 통해 얻을 수 있다.

이렇게 테스트가 만들고 테스트를 성공하게 해주는 코드를 작성하는 개발방법이 있다. 이를 테스트 우선 개발TDD:Test Driven Development이라고도 하며, ‘실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다’를 기본 원칙으로 하고 개발한다. 테스트를 먼저 만들고 테스트를 통과하도록 코드를 만들기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들 수 있다. 덕분에 코드에 대한 피드백을 매우 빠르게 받을 수 있다.

개발한 코드의 오류는 가능한 빨리 발견할 수록 좋기 때문에 TDD는 빠른 피드백으로 잘못된 부분을 빨리 찾아내고 대응할 수 있는 장점이 있다.

테스트 코드 개선

JUnit이 @Before, @After가 @Test가 붙은 메소드가 부르기 전과 후에 자동으로 실행한다. 이를 통해 공통적인 준비작업과 정리작업을 수행할 수 있다. JUnit은 각 테스트가 영향을 받지 않고 독립적으로 실행됨을 보장하기 위해 매번 새로운 오브젝트를 만들어 테스트한다.

픽스처 : 테스트를 실행하는데 필요한 정보나 오브젝트.

일반적으로 픽스처는 여러 테스트에 반복적으로 사용되기 때문에 @Before에서 생성해두면 편리하다.

JUnit이 하나의 테스트 클래스를 수행하는 방식

  1. @Test 어노테이션이 붙은 public 테스트 메소드를 찾는다.
  2. 테스트 클래스의 오브젝트를 생성한다.
  3. @Before가 붙은 메소드를 실행한다.
  4. @Test가 붙은 메소드를 실행한다.
  5. @After가 붙은 메소드를 실행한다.
  6. 테스트 메소드들에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해 돌려준다.

스프링 테스트 적용

JUnit이 테스트 마다 오브젝트를 새로 생성하지만, 어플리케이션 컨텍스트 같은 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다. 빈은 싱글톤으로 상태를 갖지 않기 때문에 이렇게 공유하게 만들더라도 문제가 되지 않는다.

스프링 테스트 컨텍스트 프레임워크 적용

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(location="/applicationContext.xml")
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;
    ...
    
    @Before
    public void setUp() {
        this.dao = this.context.getBean("userDao", UserDao.class);
        ...
    }
}
  • @RunWith : JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션이다.
  • @ContextConfiguration : 테스트 컨텍스트가 만들어줄 어플리케이션 컨텍스트 위치를 지정한다.
  • @Autowired : 테스트 클래스에서도 스프링 프레임워크에 의해 인젝션된다

DI와 테스트

인터페이스를 두고 DI를 적용해야하는 이유

  1. 소프트웨어 개발에서 절대 변하지 않는건 없다.
  2. 인터페이스를 두고 DI를 적용하게 하면 다른 차원의 서비스 기능을 도입할 수 있다. 설정파일을 수정해서 간단히 기능을 추가하거나 제거할 수 있다.
  3. 효율적인 테스트를 손쉽게 만들기 위해서 DI를 적용해야한다. DI는 작은 단위의 테스트를 만드는데 중요한 역할을 한다.

1. 테스트 코드에 의한 DI

applicationContext.xml에 정의된 DataSource 빈을 이용해 DB 커넥션을 사용한다고 할 때, 테스트할 때 실제 운영하는 DB에 접근하여 사용한다면 문제가 될 수 있다. 이런 경우에 테스트 코드에 의한 DI를 이용하여 테스트중에 DAO가 사용할 DataSource 오브젝트를 바꿔줄 수 있다.

하지만 이 방법은 설정정보를 따라 구성한 오브젝트를 가져와 의존관계를 강제로 변경했기 때문에 사용에 주의해야한다. 한번 설정된 DataSource의 의존관계가 이후의 모든 테스트에 영향을 줄 수 있기 때문이다.

@DirtiesContext 라는 어노테이션을 사용하여 스프링 테스트 컨텍스트 프레임에게 해당 클래스의 테스트에서 어플리케이션 컨텍스트의 상태를 변경한다는 것을 알려줄 수 있다. 이 어노테이션의 붙은 클래스에는 어플리케이션 컨텍스트 공유를 허용하지 않는다. 하지만 매번 어플리케이션 컨텍스트를 만드는 것은 조금 불편하다.

2. 테스트를 위한 별도의 DI

테스트의 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정 파일을 따로 만들 수 있다. 테스트 전용 설정 파일을 만든 뒤 @ContextConfiguration 어노테이션의 locations 엘리먼트 값을 테스트용 설정파일로 변경하면 테스트에 테스트용 설정 파일을 사용할 수 있다.

3. 컨테이너 없는 DI 테스트

아예 스프링 컨테이너를 사용하지 않고 테스트를 구성할 수도 있다. 스프링 프레임워크를 사용하는 DAO 클래스들은 스프링 API를 직접 사용하지 않기 때문에 스프링 DI 컨테이너에 의존하지 않는다. 때문에 직접 오브젝트를 만들고 DI해서 테스트를 할 수 있다. DI는 객체지향 프로그래밍 스타일이기 때문에, DI를 위해 컨테이너가 반드시 필요한건 아니다.

학습 테스트로 배우는 스프링

때로는 자신이 만들지 않은 프레임워크나 라이브러리 등에 대해서 테스트를 작성해야할 때가 있다. 이러한 테스트를 학습 테스트라고 한다. 학습 테스트의 목적은 기능을 테스트로 확인하며 사용 방법을 익히려는 것이다. 테스트 코드를 만드는 과정을 통해 API의 사용방법을 익히고 자신이 가진 기술에 대한 지식도 검증할 수 있다.

학습 테스트의 장점

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
  • 학습 테스트 코드를 개발 중에 참고할 수 있다.
  • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
  • 테스트 작성에 대한 좋은 훈련이 된다.
  • 새로운 기술을 공부하는 과정이 즐거워진다.

버그 테스트

코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말한다. 버그가 발생했을 때 무작정 코드를 뒤져가며 수정을 하는 것 보단, 버그가 원인이 되어 테스트가 실패하는 테스트를 만들어 테스트가 통과되도록 어플리케이션 코드를 수정하는 편이 유용하다.

  • 테스트의 완성도를 높여준다.
  • 버그의 내용을 명확하게 분석하게 해준다.
  • 기술적인 문제를 해결하는 데 도움이 된다.