UserDaoTest 다시보기

테스트의 유용성

우리가 현재 만들었던 UserDaoTestadd()get() 메서드를 이용해서 UserDao 에서의 add()get() 을 호출합니다.

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

이를 통해서 코드나 설계의 결함을 확인할 수 있습니다.


UserDaoTest의 특징

우리가 이전에 만들었던 main() 메서드로 작성된 테스트코드를 다시 살펴봅시다.

public static void main(String[] args) throws ClassNotFoundException, SQLException{
    UserDao dao = new UserDao();

    User user = new User();
    user.setId("haecahn");
    user.setName("유해찬");
    user.setPassword("1234");

    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를 만들고 난 뒤에 바로 테스트를 하는 것이 아닌 서비스 계층과 MVC 패턴등을 모두 대충이라도 만든 후에 직접적으로 웹에서 기능들을 실행해보면서 확인을 합니다.

하지만 이러한 방법은 모든 서비스 클래스, 컨트롤러 JSP등을 모두 만들고 나서야 테스트가 가능하는게 큰 문제입니다.

그래서 문제가 발생을 한다면 어디서 발생했는지 못찾는다는 단점이 존재합니다.


작은 단위의 테스트

우리는 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직합니다.
모든 것들을 한꺼번에 몰아서 한다면 테스트 수행과정이 복잡해지고 정확한 원인을 찾기가 힘들어집니다. 이를 통해서 관심사의 분리라는 원리가 적용됩니다.

UserDaoTest는 한 가지 관심에 집중할 수 있게 작은 단위로 만들어진 테스트입니다.

그래서 서비스 오브젝트나 MVC같은 것들이 필요가 없습니다.

이렇게 작은 단위로 테스트 하는 것을 단위 테스트(Unit test) 라고 불립니다.

여기에서 말하는 단위는 크기와 범위가 딱 정해진것이 아니라 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위라고 보면 됩니다.

일반적으로는 단위는 작을수록 좋습니다. 또한, 단위를 넘어서는 다른 코드들은 신경 쓰지 않고, 참여하지도 않고 테스트가 동작할 수 있으면 좋습니다.


그렇다면 UserDaoTest는 테스트 중에 DB까지 사용이 되는데 이를 단위 테스트라고 봐도 좋은것일까요?

하지만 UserDaoTest는 이를 수행할 때 User 테이블의 내용을 비우고 테스트를 진행 했습니다.

이는 결론적으로 DB의 상태를 Test가 관장하고 있기 때문에 우리는 단위 테스트라고 부를수 있습니다.

단위 테스트가 필요한 이유는 무엇인지 살펴봅시다

우리가 단위 테스트를 하는 이유는 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지 개발자 스스로 빨리 확인 받기 위해서 우리는 단위 테스트를 사용합니다.

만약 개발자가 스스로 테스트를 받지 않느다면 나중에 클라이언트에게 주고 테스트를 받을 떄는 코드가 엄청커져서 오류를 찾기 힘들 것입니다.

이러한 단위 테스트는 우리가 만든 혹은 개선한 코드가 처음 설계하고 의도한 대로 바르게 동작했는지를 확인하기 위해서 개발자 입장에서 만든 것이므로 이를 개발자 테스트라고도 부를 수 있습니다.


자동수행 테스트 코드

UserDaoTest의 한 가지 특징은 테스트할 데이터가 코드를 통해 제공되고 테스트 작업도 코드를 통해 자동으로 실행합니다.

그래서 웹 화면에 폼을 띄우고 버튼을 누르는 일이 없습니다.

현재 우리가 만든 UserDaoTestmain() 메서드를 실행하는 가장 간단한 방법만으로 테스트의 전 과정이 자동으로 진행이 됩니다. 이러한 코드는 우리가 웹 화면에 폼을 띄우고 테스트를 직접하는 방법보다 훨씬 빨리 진행이 될 수 있습니다.

이러한 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요합니다.

또한, 테스트 코드는 어플리케이션을 구성하는 클래스 안에 테스트 코드를 포함시키는 것보단 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫습니다.

그래서 우리는 UserDaoTest 라는 별개의 클래스로 분리해서 그 안에 테스트 코드를 넣도록 했습니다.

이러한 자동으로 수행되는 코드의 장점은 자주 반복할 수 있다는 것 입니다. 이를 통해서 빠른 테스트를 할 수 있습니다.


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

우리는 지금까지의 과정을 통해서 DAO코드를 → 스프링을 이용한 코드로 만들 때 테스트가 중요한 역할을 했습니다.

우리는 그래서 DAO코드를 작성하고 바로 DAO를 테스트하는 코드를 만들어 주었기 때문에 코드를 개선하는 과정에서 실수를 테스트를 통해 바로 확인할 수 있었습니다.

만약에 그것이 아니라 처음부터 스프링을 적용한 뒤 XML로 설정을 만들고 모든 코드를 검증하려고 했다면 우리는 오류가 난다면 막막할 것입니다.

우리는 이런식으로 기능을 만들면 테스트도 함께 추가하는 방식으로 점진적인 개발이 가능해집니다.

이러한 테스트를 사용한다면 새로운 기능도 기대한 대로 동작하는지 확인을 할 수 있습니다.

또한, 새로운 기능을 추가ㅎ느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지 확인이 가능합니다.


UserDaoTest의 문제점

물론 웹을 통한 Test보다 훌륭하지만 이는 문제점이 존재합니다.

수동 확인 작업의 번거로움

우리는 add()를 통해서 User 정보를 DB에 등록을하고 이 때 넣었던 User의 정보가 get()으로 가져왔을때 같은지는 눈으로 직접확인을 해야합니다. 이를 Test코드는 확인을 해주지 않습니다.

현재는 필드가 별로 많지 않아서 직접 확인해도 많이 번거롭지는 않지만 만약 필드값이 늘어나거나 복잡하다면 이는 물론 복잡한 일입니다.


실행 작업의 번거로움

우리는 DAO가 많아지고 이를 테스트하는 main() 메서드도 많아진다면 전체 기능을 테스트해보기 위해서 main() 메서드를 엄청 많이 실행하는 수고가 필요합니다.

또한, 테스트 결과를 정리하는 것도 큰 작업이 됩니다.


UserDaoTest 개선

테스트 검증의 자동화

우선 첫 번째 문제점인 수동 확인 작업의 번거로움을 해결해 봅시다.

우리는 테스트에 대해서 성공과 실패의 두 가지 결과를 가질 수 있습니다. 또한, 실패에 대해서는 2가지로 나누어집니다.

  • 테스트 진행중 오류가 발생하는 경우
  • 테스트 작업중 에러는 발생하지 않았지만 결과값이 기댓값과 다를 경우

이러한 두 가지로 나뉘어집니다.

테스트에 에러가 발생하는 것은 콘솔에 에러 메시지와 호출 스택 정보를 통해서 확인이 가능합니다.

하지만 테스트가 실패를 한다는 것은 우리는 별도의 확인 작업이 필요합니다. get() 을 통해서 가져온 결과가 add()를 한 값과 다를 때는 “테스트 실패”, 아닌 경우는 “조회 성공”이라는 값을 나타 내도록 수정하면 다음과 같습니다.

//변경 전
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("테스트 성공");
}

이렇게 가져온 오브젝트가 같은지 equals 라는 메서드를 통해서 확인해서 검증을 할 수 있습니다.

이러한 테스트는 UserDao의 두 가지 기능이 정상적으로 동작하는지를 언제든지 손쉽게 확인할 수 있게 해줍니다.

그렇기 떄문에 이 코드의 동작에 영향을 미칠 수 있는 어떤 변화라도 생기면 언제든 다시 실행해볼 수 있습니다.

만약 스프링이 아닌 다른 프레임워크나 기술로 변화해도 우리가 만든 테스트 한 번이면 충분하게 됩니다.

우리는 만들어진 코드의 기능을 모두 점검할 수 있는 포괄적인 테스트를 만들어서 기존에 수동 테스트로는 당장 수정한 기능의 가장 간단한 케이스를 확인하기도 힘든 상황을 극복할 수 있게 되었습니다.


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

이제 main() 메서드로 만든 테스트는 테스트로서 필요한 기능은 모두 갖추었습니다. 하지만 이러한 main() 메서드로는 한계가 있습니다. 이를 해결하기 위해서 우리는 Junit을 사용할 것 입니다.

Junit 테스트로 전환

Junit은 자바로 단위 테스트를 만들때 사용하는 프레임워크입니다.

프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어합니다.

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


테스트 메서드 전환

우리가 현재 main() 메서드로 테스트가 만들어졌다는 것은 제어권을 직접 갖는다는 의미이므로 이를 일반 메서드로 옮기는 과정을 진행할 것입니다.

이때 우리는 JUnit 프레임워크가 요구하는 조건 2가지를 따라야합니다.

  • 메서드는 public 으로 선언돼야 합니다.
  • 메서드에 @Test 라는 에너테이션을 붙여야합니다.
@Test
public void addAndGet() throws SQLException{
    ApplicationContext context = new
        ClassPathXmlApplicationContext("applicationContext.xml");

        UserDao dao = context.getBena("userDao", UserDao.class); 

        /...
}

검증 코드 전환

우리는 테스트의 결과를 검증하는 if/else를 JUnit 이 제공하는 방법을 이용해서 전환해보려 합니다.

우리는 현재 equals 를 통해서 반환된 객체가 같은지를 확인합니다.

이러한 것을 assertThat 이라는 스태틱 메서드를 이용해서 비교를 하는 것으로 변환을 해줍니다.

assertThat() 은 첫 번째 파라미터의 값을 뒤에 나오는 매처 라고 불리는 조건으로 비교해서 일치하면 넘어가고 아니면 테스트가 실패하도록 합니다. 그래서 이는 “테스트 성공”과 같은 메시지를 굳이 출력할 필요는 없습니다.

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

        UserDao dao = context.getBena("userDao", UserDao.class); 

        User user = new User();
        user.setId("haechan");
        user.setName("해찬");
        user.setPassword("1234");

        dao.add(user);

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

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

JUnit 테스트 실행

우리를 테스트 메서드를 시작하기 위해서는 JUnit 프레임 워크를 시작시켜야 합니다.

그러기 위해서는 어디에서든 main() 메서드를 하나 추가하고 그 안에 JUnitCore 클래스의 main() 메서드를 호출해주는 간단한 코드를 넣으면 됩니다.

또한, 메서드 파라미터에는 @Test 테스트 메서드를 가진 클래스의 이름을 넣어줍니다. 예시는 다음과 같습니다.

import org.junit.runner.JUnitCore;

public static void main(String[] args) {

        JUnitCore.main("springbook.user.dao.UserDaoTest");
}

이런식으로 설정을 하면 결과가 나오게 됩니다.

만약 테스트가 실패한다면 총 수행한 테스트중에서 몇 개의 테스트가 실패했는지를 보여줍니다.

우리는 assertThat() 에서 검증한 결과값이 아니면 AssertionError 를 던져서 예외를 발생합니다. 그 뒤에 테스트는 중단이 됩니다.


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

JUnit은 자바의 표준 테스팅 프레임워크라고 불릴만큼 폭넓게 사용되고 있습니다.

스프링 프레임워크 자체도 JUnit 프레임워크를 이용해서 테스트를 만들어가면서 개발되었습니다. 그래서 스프링도 JUnit을 이용합니다. 그래서 이러한 기능을 배워보도록 하겠습니다.

테스트 결과의 일관성

우리는 현재 JUnit을 적용해 깔끔한 테스트 코드를 만들었습니다. 하지만 불편한 점도 존재합니다.

우리는 테스트를 실행할 때 DB의 USER 테이블의 데이터를 모두 사젝해줘야 합니다. 만약 이전 테스트의 정보가 있으면 에러가 발생할 것입니다.

이를 살펴본다면 외부의 (DB 에) 상태에 따라서 성공하기도 실패하기도 한다는 점입니다.

이러한 코드는 외부요인에 따라서 변하기 때문에 좋은 테스트라고 할 수 없습니다.

만약 DB의 USER 테이블에 문제가 있다면 이를 테스트를 실행하고 난 뒤 마다 테스트를 수행하기 이전 상태로 만들어 주는 것입니다.

이렇게 한다면 테스트를 수행하기 이전 상태로 만들어줄 수 있습니다.


deleteAll()의 getCount() 추가

일관성있는 결과를 만들기 위해서 다음 코드를 추가합니다.

// deleteAll() 메서드
public void deleteAll() thorws SQLException{
    Connection c = dataSource.getConnection();

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

    ps.close();
    c.close();
}

// getCount() 메서드
public int getCount() throws SQLException{
    Connection c = dataSource.getConnection();

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

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

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

    return count;
}

deleteAll()과 getCount()의 테스트

우리는 새로운 기능을 추가했기 때문에 추가된 기능에 대한 테스트를 만들어 봅시다.

하지만 새로 만든 deleteAll()getCount()는 독립적으로 자동 실행되는 테스트를 만들기가 애매합니다.

만약, 굳이 테스트를 한다면 수동으로 데이터를 넣은 뒤 deleteAll() 메서드로 삭제를 하고 테이블에 남은 게 있는지 확인해야 하면 됩니다. 하지만 이런 방법은 자동회되서 반복적으로 실행 가능한 테스트 방법은 아닙니다.

그래서 우리는 addAndGet() 테스트를 확정하는 방법을 사용해서 이를 검증합니다.

이에 대한 테스트의 불편한 점은 수동으로 USER 테이블의 내용을 모두 삭제해줘야합니다.

이를 사용하면 매번 USER 테이블의 모든 내용을 삭제할 수 있어 테이블을 수동으로 삭제하지 않아도 됩니다.

하지만 이는 부족합니다. 우선 deleteAll() 의 검증이 되지 않았는데 addAndGet() 의 검증을 진행할 수 없습니다. 그래서 getCount() 를 사용해서 검증을 합니다. 만약 모두 삭제가 되었으면 count = 0 이 될 것입니다.

일단 우리는 getCount()를 검증을 해봅시다.

앞에서 add()는 검증이 완료되었기 때문에 add()를 통해서 만약, 데이터를 넣으면 count가 바뀌는 것을 확인하면 검증이 가능합니다. 이를 통해서 deleteAll()을 하고 count가 0이 되는것을 보고 난 뒤에 이도 검증을 끝마칠수 있습니다.

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

        .../

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

        User user = new User();
        user.setId("haechan");
        user.setName("해찬");
        user.setPassword("1234");

        dao.add(user);

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

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

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

이제 테스트를 실행해본다면 몇번을 실행하도 계속 성공할 것입니다. 이러한 addAndGet(0) 을 통해서 수동으로 데이터를 삭제할 일이 사라졌습니다.

우리는 이러한 방식말고도 다른 방식이 존재합니다. addAndGet() 테스트를 마치기 전에 테스트가 변경하거나 추가한 데이터를 모두 원래 상태로 만들어 주는 것도 한가지 방법입니다.

이것도 나쁜방법은 아니지만 addAndGet() 테스트 실행 이전에 다른 이유로 USER 테이블에 데이터가 들어가 있으면 테스트가 실패할수도 있습니다. 그래서 테스트하기 전에 테스트 실행에 문제가 되지 않는 상태를 만들어주는 편이 좋습니다.


포괄적인 테스트

우리는 getCount(0) 를 사용해서 메서드에 테스트를 적용했지만 우리는 테이블이 비어있는 경우와 한번호출하는 경우만 사용이 가능합니다

이러한 우리가 생각하지도 못하는 문제들이 숨어있기 때문에 성의없게 테스트 코드를 작성하면 안됩니다.

getCount() 테스트

우리는 이러한 getCount() 테스트를 좀더 꼼꼼하게 만드려고 합니다.

이번에는 여러 개의 User를 등록해가면서 getCount() 의 결과를 매번확인하려고 합니다.

이를 위해서 addAndGet 메서드를 추가하는 것은 좋은 생각이 아닙니다. 왜냐하면 테스트 메서드는 한 번에 한 가지 검증 목적에만 충실해야 하기 때문입니다.

JUnit은 하나의 클래스 안에 여러 개의 테스트 메서드가 들어가는 것을 허용합니다. 그렇기 때문에 이를 메서드를 여러개 만들어서 사용을합니다.

우선 테스트를 만들기 전에 먼저 User 클래스에 한 번에 모든 정보를 넣을 수 있도록 초기화가 가능한 생성자를 추가합니다.

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

public User(){ // 기본생성자 만들기

}

이렇게 변경하면 addAndGet() 도 다음과 같이 수정이 가능합니다.

UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User("gyumee", "박성철", "springno1");

이제 getCount() 에 대한 테스트 메서드를 작성해보겠습니다.

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

    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("gyumee", "박성철", "springno1");
    User user2 = new User("hachan", "유해찬", "springno2");
    User user3 = new User("chayoo", "찬해유", "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));
}

이렇게 작성을 하면 별 문제 없이 실행이 됩니다.

하지만 이는 특정한 테스트 메서드의 실행 순서를 보장해주지는 안씁니다.

만약, 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것입니다.

그래서 우리는 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야합니다.


addAndGet() 테스트 보완

이제 addAndGet() 을 보완해봅시다. 이는 우리는 현재 add() 는 여러번 하면서 검증을 완료하였지만 get() 에 대해서는 id를 조건으로만 찾았기 때문에 이는 부족한 감이 있습니다. 그래서 검증이 부족한 감이 있습니다.

이를 User의 id를 파라미터로 전달하여 get()을 실행하게 할 것입니다.

그래서 get()에 대한 테스트 기느이 보완된 addAndGet() 테스트코드를 살펴봅시다.

@Test
public void addAndGet() throws SQLExeption{
    /...
    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("haechan", "유해찬", "spring1");
    User user2 = new User("chanyoo", "찬해유", "spring2");

    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 userget2 = dao.get(user2.getId());
    assertThat(userget2.getName(), is(user2.getName());
    assertThat(userget2.getPassword(), is(user2.getPassword());

}

이를 통해서 get() 메서드에 대해서 더욱 정확히 확인할 수 있습니다.


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

get() 메서드에 전달된 id 값에 해당하는 사용자 정보가 없으면 어떻게 될지 생각해봅시다.

우리는 이를 2가지 방법으로 처리할 수 있습니다.

  • null과 같은 값을 리턴하는 방식
  • id에 해당하는 정보를 찾을 수 없다고 예외를 던지는 방식입니다.

우리는 후자를 사용할 것입니다.

스프링에서는 데이터 액세스 예외 클래스가 있습니다. 우리는 그 예외인 EmptyResultDataAccessException 예외를 사용할 것입니다.

만약, get()을 실행 했을 때 결과에 아무것도 없으면 예외를 던지게 하면됩니다. 만약 예외가 던져지게 되면 테스트 메서드의 실행은 중단되고 실패합니다.

하지만 우리는 현재 EmptyResultDsataAccessException 이라는 예외가 던져져야 우리는 테스트가 성공을 했구나? 라는 것을 알 수 있습니다. 이런 특별한 상황에는 assertThat() 을 사용할 수 없습니다.

그래서 이런경우에는 JUnit의 기능을 사용할 수 있습니다. 확인해 봅시다.

@Test(excepted=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException{
    ApplicationContext context = new GenericXmlApplicationContext("appplicationContext.xml");

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

    dao.get("unknown_id");
}

@Test 에서 excepted 엘리먼트를 사용해서 우리는 기대하는 예외클래스를 넣어주면 확이니 가능합니다.

만약, excepted 라는 엘리먼트를 추가하면 예외가 던져지면 성공이고 던져지지 않으면 실패입니다.


테스트를 성공시키기 위한 코드의 수정

이제부터 우리는 get() 메서드 코드를 수정해서 이 테스트가 성공하도록 할 것입니다.

다음과 같이 작성을 하면 만들 수 잇습니다.

// 데이터를 찾지 못하면 예외를 발생시키도록 수정한 get() 메서드
public User get(String id) throws SQLException{
    /...
    ResultSet rs = ps.excuteQuery();

    User user = null;
    if(rs.next()){
        user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

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

    if(user == null) throw new EmptyResultDataAccessException(1);

    return user;
    }
}

이런식으로 get() 메서드에서 데이터가 없을 때 예외를 발생시키면 테스트는 성공할 것입니다.


포괄적인 테스트

사실 우리가 작성한 코드들은 간단해서 테스트가 없어도 문제를 살펴볼 수 잇습니다.

하지만 이런식으로 DAO의 메서드에 대해서 포괄적인 테스트를 만들어두는 편이 훨씬 안전하고 유용합니다.

또한, 여러가지 이점이 존재합니다.

개발자들은 이상한 습성이 있습니다. 바로 성공하는 테스트만 골라서 만드는 것입니다. 하지만 이러한 것은 큰 문제점입니다. 스프링의 창시자도 ”항상 네거티브 테스트를 먼저 만들라” 라는 조언을 하고 잇습니다. 그렇기 때문에 부정적인 케이스 부터 먼저 만드는 습관을 들이는 것이 좋습니다.


테스트가 이끄는 개발

우리는 get() 메서드의 예외 테스트를 만들면서 UserDao 코드를 수정을 계속해서 반복하였습니다. 이는 테스트를 통해서 우리는 개발을 진행하였습니다.

물론 테스트할 코드도 없이 테스트 코드를 만드는 것은 이상하지만 이러한 순서를 따라서 개발하는 TDD 방식의 개발도 존재합니다, 우리는 이런 개발 방법을 적극적으로 권장합니다.=


기능설계를 위한 테스트

우리는 가장 먼저 존재하지 않는 id로 get() 메서드를 실행한다면 특정한 예외가 던져져야한다. 라는 방식으로 만들어야 할 기능을 결정하였습니다.

하지만 테스트할 코드도 없는데 어떻게 테스트할까요? 이는 추가하고 싶은 기능을 코드로 표현하려고 했기 때문에 가능합니다.

우리는 이를 조건, 행위, 결과로 나타낼 수 있습니다.

getUserFailure()

타입 단계 내용 코드
조건 어떤 조건을 가지고 가져올 사용자 정보가 존재하지 않는 경우에 dao.deleteAll()
assertThat(dao.getCount()m is(0));
행위 무엇을 할 때 존재하지 않는 id로 get()을 실행하면 get("unknown_id");
단계 어떤 결과가 나온다 특별한 예외가 던져진다. @Test(expected=EmptyResultDataAccessEception.class)

이러한 테스트 코드는 마치 잘 작성된 하나의 기능정의서처럼 보입니다.

테스트가 실패한다면 설계한 대로 코드가 만들어지지 않는다는 것이기 때문에 이를 통해서 코드를 계속 다듬어갑니다.


테스트 주도 개발

우리는 이러한 방식을 테스트 주도 개발(TDD, Test Driven Development)라고 합니다.

또는 테스트 우선 개발 이라고도 부릅니다.

테스트 주도 개발의 중요한 점은 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다. 라는 것입니다. 이에 대한 장점은 다음과 같습니다.

  • TDD는 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어 낼 수 있습니다.
  • 또한, 코드를 만들고 테스트를 수행할 때까지 걸리는 시간은 0입니다.
  • 매번 테스트가 성공하는 것을 보면서 작성한 코드에 대한 확신을 가질 수 있습ㄴ디ㅏ.

이러한 TDD를 할 때는 테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 짧게 가져가도록 권장합니다.

우리는 머리속에서 예를들어 ‘이런 조건의 값이 들어오면 코드의 흐름과 조건을 따라서 이렇게 진행되서 이런결과의 값이 나오겠다.’ 라는 식의 시뮬레이션을 계속합니다.

현재 우리는 머리속으로만 이를 시뮬레이션을 하는데 그러면 제약도 심하고 오류도많고 다시 반복하기가 힘듭니다.

이러한 아이디어들을 바로 코드로 끄집에 내는 것이 TDD 입니다.


테스트 코드 개선

우리는 3개의 테스트 메서드를 만들었습니다. 이제 테스트 코드에 대해서 리펙토링 해봅시다.

일단 UserDao 를 가져오는 부분이 계속 반복이 됩니다. 이를 메서드를 통해서 분리하는 것 보다는

JUnit 이 제공하는 기능을활용해서 변경해봅시다.

@Before

우리는 @Before 어노테이션을 사용해서 setUp() 이라는 메서드를 만들어서 테스트 메서드에서 반복되는 ㄱ코드를 넣고 이를 로컬로 private 하게 변수를 선언해서 변수에 저장하여 언제든지 쓸수 있게 해봅시다.

import org.junit.Before;

public class UserDaoTest{
    private UserDao dao;

    @Before
    public void setUp(){
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.dao = context.getBean("userDao", UserDao.class);
    }

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

    /...
}

이런식으로 작성하게 되면 테스트 코드를 실행 전에 이를 얻게 됩니다.

왜 이렇게 작동하는지에 대해서는 JUnit 이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식을 살펴봅시다.

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

세부적으로는 더 복잡하지만 크게보면 다음과 같습ㄴ디ㅏ.

그렇기 때문에 @Before 이나 @After 메서드를 자동으로 실행합니다.

하지만, @Before@After 메서드를 테스트 메서드에서 직접 호출하지 않기 때문에 이를 인스턴스 변수를 이용해서 저장하여 테스트 메서드에서 사용하게 만들면 됩니다.

이러한 테스트 메서드를 실행할때마다 새로운 오브젝트를 만들고 있습니다. JUnit은 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해서 매번 새로운 오브젝트들을 만들게 한것이 그 이유입니다

그래서 우리는 인스턴스 변수를 부담없이 사용을 할 수 있습니다. 다음 텍스트 메서드가 실행 될 때는 새로운 오브젝트가 만들어져서 다 초기화 될 것이기 때문입니다.

만약, 테스트 메서드의 일부에서만 공통적으로 사용되는 코드가 있다면 어떻게 해야할까요?

이때는 @Before 보다는 일반적으로 우리가 사용하는 메서드 분리 방식을 이용하는 것이 바람직 합니다.


픽스처

픽스처(fixture)는 테스트를 수행하는 데 필요한 정보나 오브젝트를 말합니다.

이는 여러 테스트에서 반복되어 사용되기 때문에 @Before 를 통해서 메서드를 생성해두면 편합니다.

우리가 짠 코드를 본다면 현재 getUserFailure() 라는 테스트를 제외하고는 User 에 대한 오브젝트가 사용이 되고 있습니다. 우리는 일부에서만 공통적으로 사용하기 때문에 메서드 분리 방식을 이용한다고 생각하지만 다른 테스트들은 거의다 User 에 대해서 사용할 것이기 때문에 @Before 를 통해서 빼주도록 합니다.

그래서 다음과 같은 코드를 생성할 수 있습니다.

public class UserDaoTest{
    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setUp(){
        /...
        this.user1 = new User("haechan", "유해찬", "spring1");
        this.user2 = new User("chanyoo", "찬유해", "spring2");
        this.user3 = new User("yoohae", "해유찬", "spring3");
    }
} 

+ Recent posts