2장-테스트

9 분 소요

2장 테스트

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

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

또한, 테스트스프링을 학습하는데 있어 가장 효과적인 방법의 하나다.

2장에서는 테스트란 무엇이며, 그 가치와 장점, 활용전략, 스프링과의 관계를 살펴본다. 그리고 대표적인 테스트 프레임워크를 소개하고, 이를 이용한 학습전략도 알아볼 것이다.

2.1 테스트의 유용성

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

2.1.2 UserDaoTest의 특징

이 테스트 코드의 내용을 정리해보면 다음과 같다.

  • 자바에서 가장 손쉽게 실행 가능한 main() 메소드를 이용한다.
  • 테스트할 대상인 UserDao의 오브젝트를 가져와 메소드를 호출한다.
  • 테스트에 사용할 입력 값(User 오브젝트)을 직접 코드에서 만들어 넣어준다.
  • 테스트의 결과를 콘솔에 출력해준다.
  • 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공 메시지로 출력해준다.

public class UserDaoTest {
    public static void main(String[] args) throws SQLException {
        ApplicationContext context = new GenericXmlApplicationContext (
            "applicationContext.xml");
        UserDao dao = context.getBean("userDao", UserDao.class);

        User user = new User();
        user.setId("user");
        user.setName("백기선");
        user.setPassword("married");

        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() + "조회 성공");
    }
}

이 테스트 방법에서 가장 돋보이는건, main() 메소드를 이용해 쉽게 테스트 수행을 가능하게 했다는 점과 테스트할 대상인 UserDao를 직접 호출해서 사용한다는 점이다.

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

  • 웹 화면을 통해 값을 입력하고, 기능을 수행하고, 결과를 확인하는 방법은 가장 흔히 쓰이는 방법이지만, DAO에 대한 테스트로서는 단점이 너무 많다.
  • DAO뿐만 아니라 서비스 클래스, 컨트롤러, JSP 뷰 등 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다는 점이 가장 큰 문제다.
  • 테스트를 하는 중에 에러가 나거나 테스트가 실패했다면, 과연 어디에서 문제가 발생했는지를 찾아내야 하는 수고도 필요하다.

  • 그렇다면 테스트를 어떻게 만들면 이런 문제를 피할 수 있고, 효율적으로 테스트를 활용할 수 있을지 생각해보자.

작은 단위의 테스트

  • 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다.
  • 따라서 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.

  • 이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트(unit test)라고 한다.

  • 때로는 웹 사용자 인터페이스부터 시작해서 DB에 이르기까지의 애플리케이션 전 계층이 참여하고, 또 단순 사용자 등록 작업 하나가 아니라, 초기 등록에서부터 시작해서 등록이 성공하면 로그인하고, 각종 기능을 모두 사용한 다음에 로그아웃까지 하는 전과정을 하나로 묶어서 테스트할 필요가 있다.
  • 각 단위 기능은 잘 동작하는데 묶어놓으면 안되는 경우가 종종 발생하기 때문이다.

  • 따라서 이런 길고 많은 단위가 참여하는 테스트도 언젠가는 필요하다.

  • 작은 단위로 나눠서 테스트 하는 단위 테스트가 필요한 이유는 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서다.

자동수행 테스트 코드

  • UserDaoTest의 한 가지 특징은 테스트할 데이터가 코드를 통해 제공되고, 테스트 작업 역시 코드를 통해 자동으로 실행한다는 점이다.
  • UserDaoTest는 자바 클래스의 main() 메소드를 실행하는 가장 간단한 방법만으로 테스트의 전 과정이 자동을 진행된다.
  • 이렇게 테스트자동으로 수행되도록 코드로 만들어지는 것이 중요하다.

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

  • 자동으로 수행되는 테스트의 장점자주 반복할 수 있다는 것이다.

지속적인 개선과 점진적인 개발을 위한 테스트

  • 만약 처음부터 스프링을 적용하고 XML 설정을 만들고 모든 코드를 다 만들고 난 뒤에 이를 검증하려고 했다면, 아마 쏟아지는 에러 메시지에 기가 질려서 무엇을 해야할지 몰라 막막해졌을지도 모르겠다.

  • 그래서 일단은 단순 무식한 방법으로 정상동작하는 코드를 만들고, 테스트를 만들어뒀기 때문에 매우 작은 단계를 거쳐가면서 계속 코드를 개선해나갈 수 있었다.

  • 테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전ㅇ히 잘 동작하는지를 확인할 수도 있다.

2.1.3 UserDaoTest의 문제점

UserDaoTest가 UI까지 동원되는 번거로운 수동 테슽에 비해 장점이 많은 건 사실이다. 하지만 만족스럽지 못한 부분도 있다.

  1. 수동 확인 작업의 번거로움
    • UserDaoTest는 테스트를 수행하는 과정과 입력 데이터의 준비를 모두 자동으로 진행하도록 만들어졌다. 하지만 여전히 사람의 눈으로 확인하는 과정이 필요하다.
  2. 실행 작업의 번거로움
    • 좀 더 편리하고 체계적으로 테스트를 실행하고 그 결과를 확인하는 방법이 절실히 필요하다.

2.2 UserDaoTest 개선

UserDaoTest의 두 가지 문제점을 개선해보자.

2.2.1 테스트 검증의 자동화

첫번째 문제점인 테스트 결과의 검증 부분을 코드로 만들어보자.

수정 전 테스트 코드

System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + "조회 성공");

수정 후 테스트 코드

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("조회 테스트 성공"));
}
  • add()를 통한 등록 자체는 별다르게 검증할 것이 없다.
  • get()을 통한 검증 과정에서 사실 add()의 작업도 함께 확인하는 것이다.

  • 결국 테스트개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것이다.

2.2.2 테스트의 효율적인 수행과 결과 관리

  • main() 메서드로 만든 테스트는 테스트로서 필요한 기능은 모두 갖춘 셈이다.
  • 하지만 좀 더 편리하게 테스트를 수행하고 편리하게 결과를 확인하려면 단순한 main() 메서드로는 한계가 있다.
  1. 일정한 패턴을 가진 테스트를 만들 수 있고
  2. 많은 테스트를 실행시킬 수 있으며
  3. 테스트 결과를 종합해서 볼 수 있고
  4. 테스트가 실패한 곳을 빠르게 찾을 수 있는 기능을 갖춘 테스트 지원 도구
  5. 테스트 지원 도구에 맞는 테스트 작성방법이 필요하다.
  • 이를 위해 프로그래머를 위한 자바 테스팅 프레임워크라고 불리는 JUnit자바 테스트 지원 도구로 활용된다.
  • JUnit은 이름 그대로 자바로 단위 테스트를 만들 때 유용하게 쓸 수 있다.

JUnit 테스트로 전환

  • 프레임워크에서 동작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.

테스트 메소드 전환

  • 기존에 만들었던 main() 메서드 테스트는 그런 면에서 프레임워크에 적용하기엔 적합하지 않다.
  • 가장 먼저 할 일은 main() 메서드에 있던 테스트 코드를 일반 메소드로 옮기는 것이다.
  • 새로 만들 테스트 메소드는 JUnit 프레임워크가 요구하는 조건 두가지를 따라야 한다.
  1. 메서드가 public으로 선언돼야 하는 것이고
  2. 메소드 @Test라는 애노테이션을 붙여주는 것이다.
import org.junit.Test;
...
public class UserDaoTest {
    @Test // JUnit에게 테스트용 메소드임을 알려준다.
    public void addAndGet() throws SQLException { // JUnit 테스트 메소드는 반드시 public으로 선언돼야 한다.
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        UserDao dao = context.getBean("userDao", UserDao.class);
        ...
    }
}
  • UserDaoTest의 메소드명은 이왕이면 테스트의 의도가 무엇인지 알 수 있는 이름이 좋다.
  • 또한 public 액세스 권한을 주는 것을 잊으면 안된다.
  • 마지막으로 @Test 애노테이션을 붙여주면 된다.

검증 코드 전환

  • 테스트의 결과를 검증하는 if/else 문장을 JUnit이 제공하는 방법을 이용해 전환해본다.
  • if 문장의 기능을 assertThat이라는 스태틱 메소드를 이용해 다음과 같이 변경할 수 있다.
  • assertThat(user2.getName(), is(user.getName()));
  • assertThat() 메소드는 첫번째 파라미터의 값을 뒤에 나오는 매처(matcher)라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고 아니면 테스트가 실패하도록 만들어준다.
  • is()는 매처의 일종으로 equals()로 비교해주는 기능을 가졌다.

JUnit을 적용한 UserDaoTest

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
...
public class UserDaoTest {
    @Test
    public void addAndGet() throws SQLException {
        ApplicationContext context = new GenericXmlApplicationContext (
            "applicationContext.xml");

        UserDao dao = context.getBean("userDao", UserDao.class);
        User user = new User();
        // JUnit 테스트로 전환하는 김에 새로운 기분으로 테스트 데이터도 바꿔보자.
        user.setId("gyumee");
        user.setName("박성철");
        user.setPassword("springno1");

        dao.add(user);

        User user2 = dao.get(user.getId());

        assertThat(user2.getName(), is(user.getName));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }
}

JUnit 테스트 실행

JUnit 프레임워크를 이용해 앞에서 만든 테스트 메소드를 실행하도록 코드를 만들어보자.

JUnit을 이용해 테스트를 실행해주는 main() 메소드

import org.junit.runner.JUnitCore;
...
public static void main(String[] args) {
    JUnitCore.main("springbook.user.dao.UserDaoTest");
}

2.2 개발자를 위한 테스팅 프레임워크 JUnit

  • JUnit 테스트 작성방법과 실행 방법은 알고 있어야 한다.
  • 스프링의 기능을 익히기 위해서라도 JUnit은 꼭 사용할줄 알아야한다.

2.3.1 JUnit 테스트 실행 방법

  • 가장 좋은 JUnit 테스트 실행 방법은 자바 IDE에 내장된 JUnit 테스트 지원 도구를 사용하는 것이다.

IDE

  • 대부분의 자바 개발자가 사용하고 있는 사실상의 표준 자바 IDE인 이클립스는 오래전부터 JUnit 테스트를 지원하는 기능을 제공하고 있다.
  • JUnit은 한 번에 여러 테스트 클래스를 동시에 실행할 수도 있다.
  • 이클립스의 단축키를 기억해두면 더욱 빠르고 편하게 테스트할 수도 있다.
  • Alt + Shift + X + T를 순서대로 누르면 선택해둔 클래스나 패키지, 프로젝트의 테스트가 바로 실행된다.

빌드툴

  • 프로젝트의 빌드를 위해 ANT나 메이븐(Maven)과 같은 빌드툴과 스크립트를 사용하고 있다면, 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용해 JUnit 테스트를 실행할 수도 있다.
  • 그런데 여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야될 때도 있다.
  • 이런 경우에는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다.

2.3.2 테스트 결과의 일관성

  • 테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다.
  • 예를 들어, UserDaoTest를 실행하기 전에 DB의 USER 테이블 데이터를 모두 삭제해줘야 할 때였다.
  • 깜빡 잊고 그냥 테스트를 실행했다가는 이전 테스트를 실행했을 때 등록됐던 사용자 정보와 기본키가 중복된다면서 add() 메소드 실행 중에 에러가 발생할 것이다.
  • 문제는 테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다는 점이다
  • 가장 좋은 해결책은 addAndGet() 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서, 테스트를 수행하기 이전 상태로 만들어주는 것이다.
  • 이로써 테스트를 아무리 여러 번 반복해서 실행하더라도 항상 동일한 결과를 얻을 수 있다.

deleteAll()의 getCount() 추가

일관성 있는 결과를 보장하는 테스트를 만들기 위해 준비할 것은 UserDao에 새로운 기능을 추가해주는 일이다.

deleteAll

  • deleteAll() 메소드로, USER 테이블의 모든 레코드를 삭제해주는 간단한 기능이 있다.
public void deleteAll() throws SQLException {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("delete from users");
    ps.executeUpdate();

    ps.close()
    c.close()
}

getCount()

  • getCount()메서드는 USER 테이블의 레코드 개수를 돌려준다.
public int getCount() throws SQLException {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("select count(*) form users");

    ResultSet rs = ps.executeQuery();
    rs.next();
    int count =rs.getInt();

    rs.close();
    ps.close();
    c.close();

    return count;
}

deleteAll()과 getCount()의 테스트

  • deleteAll()과 getCount()를 적절히 모두 활용한다
  • getCount()로 검증작업을 추가한다.

deleteAll()과 getCount()가 추가된 addAndGet() 테스트

@Test
public void addAndGet() throws SQLException {
    ...

    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    User user = new User();
    user.setId("gyumee");
    user.setName("박성철");
    user.setPassword("springno1");

    dao.add(user);
    assertThat(dao.getCount(), is(1));

    User user2 = dao.get(user.getId());

    assertThat(user2.getName(), is(user.getName()));
    assertThat(user2.getPassword(), is(user.getPassword()));
}

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

  • 단위 테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다.
  • 동일한 테스트 결과를 얻을 수 있는 다른 방법도 있다.
  • 테스트하기 전에 테스트 실행에 문제가 되지 않는 상태를 만들어주는 편이 더 나을 것이다.

  • 단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다는 점을 잊어선 안된다.

2.3.3 포괄적인 테스트

  • 성의없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는건 더 위험하다.

getCount() 테스트

  • @Test가 붙어 있고 public 접근자가 있으며 리턴 값이 void형이고 파라미터가 없다는 조건을 지키기만 하면 된다.

파라미터가 있는 User클래스 생성자

public User(String id, String name, String password) {
    this.id = id;
    this.name = name;
    this.password = password;
}

public User() {
    // 자바빈의 규약을 따르는 클래스에 생성자를 명시적으로 추가했을 때는 파라미터가 없는 디폴트 생성자도 함께 정의해주는 것을 잊지말자.
}
  • 모든 코드의 수정 후에는 그 수정에 영향을 받을 만한 테스트를 실행하는 것을 잊지말자.

getCount() 테스트


@Test
public void count() throws SQLException {
    ApplicationContext context = new GenericXmlApplicationContext (
        "applicationContext.xml";
    )

    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("gyumee", "박성철", "springno1");
    User user2 = new User("leegw700", "이길원", "springno2");
    User user3 = new User("bumjin", "박범진", "springno3");

    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    dao.add(user1);
    assertThat(dao.getCount(), is(1));

    dao.add(user2);
    assertThat(dao.getCount(), is(2));

    dao.add(user3);
    assertThat(dao.getCount(), is(3));
}
  • JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다.
  • 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다.

addAndGet() 테스트 보완

  • get이 파라미터로 주어진 id에 해당하는 사용자를 가져온 것인지, 그냥 아무거나 가져온 것인지 테스트에서 검증하지는 못했다.
  • 그래서 get() 메소드에 대한 테스트 기능을 좀 더 보완할 필요가 있다.

get() 테스트 기능을 보완한 addAndGet() 테스트

@Test
public void addAndGet() throws SQLException {
    ...

    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("gyumee", "박성철", "springno1");
    User user2 = new User("leegw700", "이길원", "springno2");
    // 중복되지 않는 값을 가진 두 개의 User 오브젝트를 준비해둔다.

    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    dao.add(user1);
    dao.add(user2);
    assertThat(dao.getCount(), is(2));

    User userget1 = dao.get(user1.getId());
    assertThat(userget1.getName(), is(user1.getName()));
    assertThat(userget1.getPassword(), is(user1.getPassword()));
    // 첫번쨰 User의 id로 get()을 실행하면 첫번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.

    User userget2 = dao.get(user2.getId());
    assertThat(userget2.getName(), is(user2.getName()));
    assertThat(userget2.getPassword(), is(user2.getPassword()));
    // 두번째 User에 대해서도 같은 방법으로 검증한다.
}

get() 예외조건에 대한 테스트

get() 메서드의 예외상황에 대한 테스트

@Test(expected=EmptyResultDataAccessException.class) //테스트 중에 발생할 것으로 기대하는 예외 클래스를 지정해준다.
public void getUserFailure() throws SQLException {
    ApplicationContext context = new GenericXmlApplicationContext (
        "applicationContext.xml");

    UserDao dao = context.getBean("userDao", UserDao.class);
    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    dao.get("unknown_id");
    // 이 메소드 실행 중에 예외가 발생해야 한다.
    // 예외가 발생하지 않으면 테스트가 실패한다.
}

카테고리:

업데이트:

댓글남기기