본문 바로가기
프로그래밍/Spring

[토비의 스프링 2장] 테스트

by Loper Lee 2022. 6. 8.

2장 테스트

스프링의 핵심 가치

스프링이 개발자에게 제공하는 가장 중요한 가치는 객체지향테스트다.

스프링으로 개발을 하면서 테스트를 만들지 않는다면 이는 스프링이 지는 가치의 절반을 포기하는 셈이다.

테스트의 유용성

코드만 만들어놓고 잘 돌아가겠거니 하고 무책임하게 개발을 마치려는 개발자는 아마 없을 것이다.

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

테스트의 결과가 제대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다.

결국 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.

UserDaoTest의 특징

public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        final ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        final UserDao userDao = context.getBean("userDao",UserDao.class);

        User user = new User();
        user.setId("whiteship");
        user.setName("백기선");
        user.setPassword("married");
        userDao.add(user);
        System.out.println(user.getId() + " 등록 성공");

        User user2 = userDao.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getPassword());
        System.out.println(user2.getId() + " 조회 성공");
    }
}

해당 코드의 내용을 정리하자면

  • 자바에서 가장 손쉽게 실행 가능한 main() 메소드를 이용한다.
  • 테스트할 대상인 UserDao의 오브젝트를 가져와 메소드를 호출한다.
  • 테스트에 사용할 입력값을 직접 코드에서 만들어 넣어준다.

웹을 통한 DAO 테스트 방식의 문제점

웹 화면을 통해 값을 입력하고, 기능을 수행하고 결과는 확인하는 방법은 가장 흔히 쓰이는 방법이지만, DAO에 대한 테스트로서는 단점이 너무 많다.

  • DAO뿐만 아니라 서비스, 컨트롤러, 뷰 등의 모든 레이어를 만들어야한다.
  • 테스트 도중 발생하는 에러의 발생지를 찾기 어려워진다.

작은 단위의 테스트

테스트는 가능하면 작은 단위로 쪼개서 집중 할 수 있어야 한다. 관심사의 분리라는 원리가 여기에도 적용된다. 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다.

  • 일반적으로 단위는 작을수록 좋다.
    • 단위를 넘어서는 다른 코드들은 신경쓰지 않고, 참여하지도 않고 테스트가 동작할 수 있으면 좋다.
    • 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 하는것이다.

UserDaoTest의 문제점

  • 수동 확인 작업의 번거로움
    • 콘솔에 나온값을 보고 등록과 조회가 성공했는지 확인하는건 사람의 몫이다.
  • 실행 작업의 번거로움
    • main() 메소드라도 실행하기엔 번거롭다.
    • DAO가 수백 개가 된다면 main() 메소드도 그만큼 늘어나게 된다.

테스트 검증의 자동화

if (!user.getName().equals(user2.getName())) {
        System.out.println("테스트 실패 (name)");
} else if (!user.getPassword().equals(user2.getPassword())) {
        System.out.println("테스트 실패 (password)");
} else {
        System.out.println("테스트 성공");
}

사용자가 콘솔로 확인하던 성공/실패의 결과를 자동화한 결과

⇒ 결국 프로그래머가 할일은 “테스트 성공”이 나오는지 확인하는것 뿐


Junit

자바 테스팅 프레임워크 중 하나, 이름 그대로 자바로 단위 테스트를 만들 때 유용하게 이용할 수 있다.

Junit 테스트로 전환

테스팅 프레임워크또한 제어의 역전형태를 띄고있기 때문에 main() 메소드가 필요없고, 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.

테스트 메소드 전환

import org.junit.Test;
...
public class UserDaoTest() {
        @Test
        public void addAndGet() throw SQLException {
                ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);                
                ...
        }
}
  • @Test 애너테이션을 통해서 Junit 테스트 메소드임을 알려준다.
  • Junit 메소드는 반드시 public으로 선언되어야 한다.

검증 코드 전환

assertThat(user2.getName()).isEqualTo(user.getName());
assertThat(user2.getPassword()).isEqualTo(user.getPassword());
  • JUnit이 제공하는 assertThat이라는 스태틱 메소드를 이용해 변경하였다.
  • 첫 파라미터에 나오는 값을 뒤에 나오는 매처라고 불리는 조건으로 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어 준다.

테스트 결과의 일관성

  • 테스트를 수행하기 전 이전 상태로 만들어준다면 매번 같은 결과를 얻을 수 있다.
  • UserDaoTest의 경우 매번 DB를 비워주는 작업을 반복하고, 카운트가 제대로 0이 되었는지 검증한다.

동일한 결과를 보장하는 테스트

단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다.

  • DB에 남아 있는 데이터와 같은 외부 환경에 영향을 받지 말아야 한다.
  • 테스트 실행 순서를 바꿔도 동일한 결과가 보장되어야 한다.

포괄적인 테스트

  • 평소에는 잘 동작하는것 처럼 보이지만 막상 특별한 상황이 되면 엉뚱하게 동작하는 코드를 만들었는데 테스트도 안해봤다면, 나중에 문제가 발생했을 때 원인을 찾기 힘들어서 고생하게 될지도 모른다.
  • 종종 간단한 테스트가 치명적 실수를 피할 수 있게 해주기도 한다.
  • 개발자가 테스트를 직접 만들때 자주하는 실수 중 하나가 성공하는 테스트만 골라서 만드는 것
    • 내 PC에서는 잘되는데 라는 변명은 예외적 상황을 모두 피하고 테스트하기 때문이다.
  • 스프링 창시자인 로드 존슨은 “항상 네거티브 테스트를 먼저 만들라”는 조언을 했다.
  • 즉 테스트는 긍정적인 테스트 뿐만이 아니라 부정적인 테스트도 만들어야 한다.
    • 테스트를 작성할때 부정적인 케이스를 먼저 작성하는 습관을 들이는게 좋다.

테스트가 이끄는 개발

  • 테스트 코드는 마치 잘 작성된 하나의 기능정의서처럼 보인다.
  • 만약 테스트가 실패한다면 이때는 설계한 대로 코드가 만들어지지 않았음을 바로 알 수 있다.
  • 코드를 수정하고 테스트를 수행해서 테스트가 성공하도록 애플리케이션 코드를 계속 다듬어간다.

테스트 주도 개발

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

  • 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다는 것이 TDD의 기본 원칙
  • 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
  • 테스트를 반나절 동안이나 만들고 오후 내내 테스트를 통과시키는 코드를 만드는 식의 개발은 그다지 좋은 방법이 아니다.
  • 사실 개발자의 머리속에서도 TDD와 유사하게 개발을 진행하고있다.
    • 머리속에서 진행되는 테스트는 제약이 심하고, 오류가 많고, 나중에 다시 반복하기 어렵다.

테스트 코드 개선

테스트 코드 자체가 이미 자신에 대한 테스트이기 때문에 테스트 결과가 일정하게 유지된다면 얼마든지 리팩토링을 해도 좋다.

  • @Before 중복된 코드를 분리해서 테스트 이전에 작업해둘 메서드로 분리한다.
  • @After 중복된 코드를 분리해서 테스트 이후에 작업해둘 메서드로 분리한다.
  • JUnit 프레임워크가는 스스로 제어권을 가지고 주도적으로 동작하고 개발자가 만든 코드는 프레임워크에 의해 수동적으로 실행행된다.

JUnit의 수행 순서

  1. 테스트 클래스에서 @Test 가 붙은 public/void/no parameter를 만족하는 메소드를 모드 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다
  3. @Before 가 붙은 메소드가 있으면 실행한다.
  4. @Test 가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
  5. @After 가 붙은 메소드가 있으면 실행한다.
  6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해서 돌려준다.

JUnit의 특징

JUnit은 매 테스트 실행마다 새로운 오브젝트를 생성하여 테스트간 영향을 주지않고 독립적으로 테스트를 진행할 수 있다.


스프링 테스트 적용

테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다. 하지만 어플리케이션 컨텍스트 처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.

  • @BeforeClass 스태틱 메소드를 통해서 한번만 실행되게 만들 수 있다.

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

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfigration(locations="/applicationContext.xml")
public class UserDaoTest {
        ...

@AutoWired 를 이용한 field DI 테스트

...
public class UserDaoTest {
        @AutoWired
        UserDao dao; // DI받는다.
        ...
  • 같은 타입의 인스턴스 변수
  • 같은 이름의 인스턴스 변수

테스트 코드에 의한 DI

  • @DirtiesContext

스프링 테스트에서 애플리케이션 컨텍스트는 딱 한 개만 만들어지고 모든 테스트에서 공유해서 사용한다. 따라서 애플리케이션 컨텍스트의 구성이나 상태를 테스트 내에서 변경하지 않는 것이 원칙이다. 만약 변경하면 나머지 모든 테스트를 수행하는 동안 변경된 애플리케이션 컨텍스트가 계속 사용될 것이다. 이는 바람직하지 못하다.
이럴때 @DirtiesContext라는 어노테이션을 추가해주면 된다. 이 애노테이션은 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다. 테스트 컨텍스트는 이 애노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게 하기 위해서다.

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

테스트 코드에서 빈 오브젝트에 수동으로 DI 하는 방법은 장점보단 단점이 많다.

  • 코드가 많아져 번거롭기도 하고 애플리케이션 컨텍스트를 매번 만들어야하는 부담도 있다.

때문에 별도의 테스트를 위한 application-context.xml 파일을 생성하여 관리한다.

컨테이너 없는 DI 테스트

  • @Before 메소드에서 직접 UserDao 오브젝트를 생성하고 테스트용 Datasource를 직접 DI한다.
  • 애플리케이션 컨텍스트가 만들어지지 않으니 테스트 시간이 빨리진다.
    • 하지만 JUnit은 매번 새로운 오브젝트를 만드는 단점이 있는데 UserDao는 가벼운 오브젝트는 별 부담은 없다.

침투적 기술과 비침투적 기술

  • 비침투적 기술 : 기술에 적용 사실이 코드에 직접반영되지 않음
  • 침투적 기술 : 기술과 관련된 코드나 규약 등이 코드에 등장

정리

  • 테스트는 자동화돼야 하고, 빠르게 실행할 수 있어야 한다.
  • JUnit 프레임워크를 이용한 테스트 작성이 편리하다.
  • 코드 작성과 테스트 수행의 간격이 짧을수록 효과적이다.
  • 테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것보다 나쁠 수 있다.