Skip to content

2장 테스트 #2

@yoon1fe

Description

@yoon1fe

스프링이 개발자에게 제공하는 가장 중요한 가치 중 하나는 테스트이다. 애플리케이션은 계속해서 변화하고 복잡해지는데, 이러한 변화에 대응하는 전략으로서, 내가 짠 코드를 확신하고, 변화에 유연하게 대처할 수 있는 기술이 바로 테스트 기술이다. 또한, 테스트를 작성함으로써 스프링의 다양한 기술을 활용하는 법을 이해/검증하고 실전에 적용하는 방법을 익힐 수 있다.

2.1 UserDaoTest 다시 보기

우리가 작성한 코드는 반드시 기대한 대로 동작하는지 어떤 방식으로든 테스트해야 한다. 1장에서는 UserDaoTest란 클래스를 만들고 main() 메서드를 통해서 UserDao 클래스의 메서드들이 제대로 동작하는지 테스트했다.

public class UserDaoTest {

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

    ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

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

    User user = User.builder()
        .id("yoon1fe")
        .name("yoon1fe")
        .password("1234")
        .build();

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

UserDaoTest 클래스의 내용을 정리하면 다음과 같다.

  • main() 메서드 이용
  • 테스트할 대상인 UserDao 의 오브젝트를 가져와 메서드 호출
  • User 오브젝트의 값을 직접 코드에서 입력
  • 테스트 결과를 콘솔에 출력

만약 웹을 통해서 DAO를 테스트한다면 번거로운 점이 많다. DAO뿐만 아니라 서비스 클래스, 컨트롤러, 뷰단까지 모두 만들어야 테스트가 가능하기 때문이다. 테스트를 하는 중에 에러가 나거나 테스트가 실패한다면 어디서 문제가 발생했는지 찾는 것도 쉽지 않다. 따라서 테스트는 가능한 작은 단위로 쪼개서 하는 것이 바람직하다. 관심사의 분리라는 개념과도 일맥상통한다. 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다. 이렇게 작은 단위릐 코드에 대해 테스트를 수행하는 것을 단위 테스트 unit test 라고 한다.

UserDaoTest 의 특징 중 하나는 테스트할 데이터가 코드를 통해 제공되고, 테스트 작업 또한 코드를 통해 자동으로 실행된다는 점이다. main() 메서드만 실행하면 User 오브젝트를 생성하고 임의 값을 넣고, UserDao 오브젝트를 애플리케이션 컨텍스트에서 가져와서 add() 메서드와 get() 메서드를 수행하는 등의 테스트의 전 과정이 자동으로 진행된다. 테스트는 이렇게 자동으로 수행되도록 코드로 만들어지는 것이 중요하다. 자동으로 수행되는 테스트는 자주 반복할 수 있다는 장점이 있다.

테스트 코드를 작성하면 지속적이고 점진적인 개선과 개발이 가능하다. 1장의 초난감 DAO를 개선할 때 일단 정상적으로 동작하는 코드를 만들고, 테스트를 만들어 두었기 때문에 매우 작은 단계를 거치면서 코드를 개선해나갈 수 있었다.

다음은 UserDaoTest 의 문제점이다.

  • 수동 확인 작업의 번거로움

    테스트를 수행하는 과정과 입력 데이터의 준비가 자동으로 진행되지만, 수행 결과를 사람의 눈으로 확인해야 한다. 콘솔에 값만 출력해줄뿐, 정상적인 값인지 체크하는 것은 여전히 사람의 책임이다.

  • 실행 작업의 번거로움

    main() 메서드를 실행하는 것 자체는 간단하지만, 만약 DAO가 수백, 수천 개가 된다면 그에 대한 모든 main() 메서드를 실행하는 것은 큰 수고가 필요하다.

2.2 UserDaoTest 개선

첫 번째 문제점인 테스트 결과의 검증 부분을 코드로 해결할 수 있다. 이 테스트를 통해 확인하고 싶은 점은, add() 에 전달한 User 오브젝트에 담긴 사용자 정보와 get() 을 통해 DB에서 가져온 User 오브젝트의 사용자 정보가 일치하는지 여부이다.

모든 테스트의 결과는 성공 또는 실패이다. 그 중 실패는 테스트 진행 중 에러가 발생해서 실패하는 경우와, 에러는 없었지만 그 결과가 기대한 값과 다르게 나오는 경우가 있다. 전자는 테스트 에러, 후자는 테스트 실패라고 부른다. 테스트 에러는 콘솔에 에러 메시지가 출력되기 때문에 쉽게 확인할 수 있지만, 테스트가 실패하는 것은 별도의 확인 작업과 그 결과가 있어야 알 수 있다. 사실 위의 테스트 코드는 DB에서 정상적으로 값을 가져왔다는 의미의 조회 성공 이었지, 우리가 원하는 결과를 가져왔는지 여부는 확인할 수 없었다.

    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("조회 테스트 성공!");
}

img

잘 된다.

이렇게 해서 테스트의 수행과 테스트 값 적용, 결과를 검증하는 것까지 모두 자동화했다. 하지만 main() 메서드를 이용한 테스트 방법만으로는 애플리케이션 규모가 커지고 테스트 수가 많아지면 테스트를 수행하는 일이 부담이 될 것이다. 자바로 단위 테스트를 만들 때 유용한 JUnit 이라는 자바 테스팅 프레임워크를 사용하면 번거로운 일이 많이 사라진다. JUnit 을 사용하기 위해서는 maven 프로젝트 기준 pom.xml 에 다음과 같이 디펜던시를 추가하면 된다.

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>RELEASE</version>
      <scope>test</scope>
    </dependency>

JUnit 프레임워크가 요구하는 조건은 두가지가 있다. 첫번째는 public 메서드여야 하고, 두번째는 메서드에 @Test 어노테이션을 붙여주는 것이다.

다음은 main() 메서드를 JUnit 프레임워크에서 동작하도록 수정한 것이다.

@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
  ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

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

}

main() 이란 이름 대신 테스트의 의도가 무엇인지 알 수 있는 적절한 이름을 붙여준다.

위에서 테스트 결과를 검증하기 위해 작성한 if/else 문 역시 JUnit이 제공하는 방법을 이용해 전환할 수 있다. assertThat() 이라는 스태틱 메서드는 첫 번째 파라미터의 값을 뒤에 나오는 매처라는 조건으로 비교해 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어준다. equals() 로 비교해주는 기능으로는 is() 라는 매처가 있다. 따라서 if (!user.getName().equals(user2.getName())) {...} 이 부분은 다음과 같이 바꿀 수 있다.

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

JUnit 은 예외가 발생하거나 assertThat() 에서 실패하지 않고 테스트 메서드가 종료되면 테스트가 성공했다고 인식한다.

JUnit 테스트를 실행하기 위해서는 마찬가지로 어딘가에 main() 메서드를 하나 추가하고, 그 안에 JUnitCore 클래스의 main() 메서드를 호출하면 된다. 메서드 파라미터에는 @Test 어노테이션이 붙은 테스트 메서드를 가진 클래스의 이름을 넣어준다.

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

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

하지만 JUnitCore 를 사용하는 방법 역시 테스트 수가 많아지면 관리하기 힘들어진다. 다행히 자바 IDE에 내장된 JUnit 테스트 지원 도구를 사용하면 간단하게 테스트를 실행할 수 있다.

img

테스트가 정상 종료되면 이렇게 잘 됐다고 이쁜 표시가 나온다. 만약 테스트 결과 검증에 실패한다면 다음과 같이 나온다.

img

지금까지 JUnit 을 적용해서 테스트 코드도 깔끔해졌고 실행도 편리해졌다. 하지만 매번 UserDaoTeset 테스트를 실행하기 전에 DB의 users 테이블을 모두 삭제해줘야 하는 번거로움이 있다.

여기서 생각해볼 문제는 테스트가 외부 상태에 따라 성공 또는 실패한다는 점이다. 반복적으로 테스트를 수행했을 때 테스트가 실패하기도 하고 성공하기도 한다면 이는 좋은 테스트라고 보기 어렵다.

따라서 위의 문제의 가장 단순한 해결책은 addAndGet() 테스트가 끝나면 테스트 코드에서 등록한 사용자 정보를 삭제하는 것이다. UserDao 클래스에 users 테이블의 모든 레코드를 삭제하는 deleteAll 메서드를 추가한다.

  public void deleteAll() throws SQLException {
    Connection conn = dataSource.getConnection();

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

    ps.close();
    conn.close();
  }

users 테이블의 레코드 개수를 반환하는 getCount() 메서드도 추가한다.

  public int getCount() throws SQLException {
    Connection conn = dataSource.getConnection();
    
    PreparedStatement ps = conn.prepareStatement("select count(*) from users");
    
    ResultSet rs = ps.executeQuery();
    
    rs.next();
    int count = rs.getInt(1);
    
    rs.close();
    ps.close();
    conn.close();
    
    return count;    
  }

새로 추가한 기능에 대한 테스트도 만들어야 하는데, 새로운 테스트를 만들기보다 기존에 만든 addAndGet() 테스트를 확장하는 것이 더 좋겠다. 테스트 실행 전에 deleteAll() 을 이용해서 기존 테이블의 모든 레코드를 삭제하도록 하자. 하지만 무턱대고 deleteAll() 메서드를 추가할 순 없다. deleteAll() 의 기능 자체도 아직 검증이 되지 않았기 때문이다. 그래서 getCount() 를 함께 적용해본다. 그런데 또! getCount() 메서드도 믿을 수 없다. 따라서 add() 메서드를 수행한 뒤 getCount() 의 결과를 한 번 더 확인해보겠다.

@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
  ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

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

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

  ...
    
}

사실 위의 테스트 코드는 그리 좋은 방법이 아니다. 실제 서비스되는 애플리케이션에서 users 테이블을 addAndGet() 메서드에서만 쓸리는 없기 때문에 테스트를 위해 테이블의 레코드를 모두 지우는건 말도 안되는 짓이다. 스프링에서 이런 문제를 해결해주는 테스트 방법을 제공해주는데, 이건 나중에 알아보자.

getCount() 메서드에 대한 테스트를 addAndGet() 메서드에서 진행했지만, 충분치 않다. 잘못된 시계도 하루에 두 번은 정확히 맞는 것처럼, 한 두가지 결과만 검증하고 마는 것은 상당히 위험하다. 따라서 User를 여러 개 등록해가면서 getCount() 의 결과를 확인하는 테스트가 바람직하다. 이 테스트는 addAndGet() 안에 들어있는건 좋지 않으니 새로운 테스트 메서드를 만들도록 한다.

@Test
public void count() throws SQLException, ClassNotFoundException {
  ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

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

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

  for (int i = 1; i <= 3; i++) {
    User user = User.builder()
        .id("yoon1fe" + i)
        .name("yoon1fe")
        .password("1234")
        .build();

    dao.add(user);
    assertThat(dao.getCount(), is(i));
  }
}

하나의 테스트 클래스 안에 여러 개의 테스트 메서드를 만들 수 있는데, 테스트 클래스 전체를 실행할 때의 주의점은 두 개의 테스트가 어떤 순서로 실행되는지는 알 수 없다는 것이다. 따라서, 만약 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다. 모든 테스트는 실행 순서나 다른 테스트에 상관없이 독립적으로 항상 동일한 결과를 내도록 작성해야 한다.

만약 get() 메서드에 전달된 id 값에 해당하는 사용자 정보가 DB에 없다면 어떤 결과가 나오는 것이 좋을까? null 과 같은 특별한 값을 리턴할 수도 있고, id 에 해당하는 정보를 찾을 수 없다고 예외를 던지는 방법도 있다. 후자에서는 스프링이 정의한 데이터 액세스 예외 클래스를 사용할 수 있다.

이런 경우를 테스트하는 방법은 특정 예외가 던져지면 테스트가 성공한 경우로 간주하는 것이다. 따라서 위에서 썼던 assertThat() 메서드처럼 리턴 값을 비교하는 방법으론 검증할 수 없다.

이런 경우를 위해 JUnit 에서 예외 조건 테스트를 위한 방법을 제공해주는데, @Test 어노테이션의 expected 옵션이다.

  @Test(expected = EmptyResultDataAccessException.class)
  public void getUserFailure() throws SQLException, ClassNotFoundException {
    ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

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

이제 위의 테스트가 성공하도록 get() 메서드를 수정해보자.

public User get(String id) throws ClassNotFoundException, SQLException {
    Connection conn = dataSource.getConnection();

    PreparedStatement ps = conn.prepareStatement("select * from users where id = ?");
    ps.setString(1, id);

    ResultSet rs = ps.executeQuery();

    User user = null;
    if (rs.next()) {
      user = User.builder()
          .id(rs.getString("id"))
          .name(rs.getString("name"))
          .password(rs.getString("password"))
          .build();
    }

    rs.close();
    ps.close();
    conn.close();

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

    return user;
  }

개발자가 테스트 코드를 작성할 때 자주 하는 실수가 있는데, 바로 성공하는 테스트만 골라서 만드는 것이다. 개발자 스스로 코드가 잘 돌아가는 케이스를 상상하면서 코드를 만드는 경우가 일반적이기 때문에, 문제가 될 만한 상황을 피해서 코드를 만들기 마련이다.

스프링의 창시자인 로드 존슨은 "항상 네거티브 테스트를 먼저 만들라"고 했다. 그래서 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 것이 좋다. 예를 들어, get() 메서드의 경우, 존재하지 않는 id 가 주어졌을 때 어떻게 처리되어야 하는지를 먼저 결정하고 이를 확인하는 테스트를 먼저 만들려고 하는 것이다.

getUserFailure() 메서드를 작성할 때, 테스트할 코드를 보고 얘를 어떻게 테스트할까라고 생각하면서 작성한 것이 아니라, 추가하고싶은 기능을 코드로 표현하려 했기 때문에 가능했다. 만들고 싶은 기능에 대한 조건, 행위, 결과에 대한 내용을 정리해보면 다음과 같다.

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

이처럼 작성해야 할 코드를 검증하는 테스트 코드부터 만들고, 그 후에 테스트할 코드를 작성하는 것이 테스트 주도 개발(TDD, Test Driven Development) 이다. "실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다"는 것이 TDD의 기본 원칙이다. 이 원칙을 따른 모든 코드는 테스트로 검증된 것으로 볼 수 있다.

정신없이 개발하다 보면 테스트 코드 작성을 뒷전으로 미루기 마련인데, TDD는 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만들기 때문에 테스트를 꼼꼼하게 만들 수 있다.

TDD에서는 테스트 작성하고 이를 성공시키는 코드를 만드는 주기를 가능한 짧게 가져가도록 권장한다. 이렇게 되면 개발한 코드의 오류를 빨리 발견할 수 있다. 그리고 빨리 발견된 오류는 쉽게 대응할 수 있다.

이쯤에서 UserDaoTest 를 리팩토링 해보겠다. 가장 먼저 애플리케이션 컨텍스트를 만드는 부분과 UserDao 를 가져오는 부분이 반복된다. 이렇게 중복되는 코드는 별도의 메서드로 뽑아 내는 것이 가장 쉬운 방법이지만, 여기서는 JUnit이 제공해주는 기능을 활용할 수 있다. JUnit4의 @Before 어노테이션을 사용하면 테스트 메서드를 실행하기 전에 이를 먼저 실행해준다. 참고로 JUnit5에서는 @BeforeEach 로 대체되었다.

@Before 어노테이션이 붙은 setUp() 메서드를 만들어서 여기에 반복되는 부분을 넣어준다.

public class UserDaoTest {

  private UserDao dao;

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

img

잘된다.

JUnit 프레임워크가 테스트 메서드를 실행하는 과정을 알아보자. 간단히 단계를 정리하면 아래와 같다.

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

단, @Before@After 가 붙은 메서드를 테스트 메서드에서 직접 호출하지 않기 때문에 필요한 정보나 오브젝트는 인스턴스 변수를 활용해야 한다.

또한, 각 테스트 메서드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다는 점이 중요하다. 테스트 클래스마다 하나의 오브젝트를 만들어놓고 사용하는 편이 성능면에서 더 효율적이지만, 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 이렇게 했다고 한다.

만약 테스트 메서드 중 일부만 공통적으로 사용하는 코드가 있다면 @Before 를 사용하기보다, 메서드를 따로 분리해서 직접 호출하는 편이 낫다.

테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처fixture 라고 한다. 이러한 픽스처는 일반적으로 여러 테스트에서 사용되기 때문에 @Before 메서드를 이용해 생성해두는 것이 편하다. add() 메서드에 넘겨지는 User 오브젝트들도 픽스처라고 볼 수 있는데, 얘도 @Before 에서 생성하도록 하는 것이 좋겠다.

public class UserDaoTest {

  private User user1;
  private User user2;
  private User user3;

  @Before
  public void setUp() {
    ...
      
    this.user1 = User.builder().id("yoon1fe").name("yoon1fe").password("1234").build();
    this.user2 = User.builder().id("yoon2fe").name("yoon2fe").password("1234").build();
    this.user3 = User.builder().id("yoon3fe").name("yoon3fe").password("1234").build();
  }
  
  ...
    
}

2.4 스프링 테스트 적용

현재 코드는 @Before 메서드가 테스트 메서드의 개수만큼 반복되기 때문에 애플리케이션 컨텍스트도 그만큼 만들어진다. 애플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화한다. 이때 어떤 빈은 초기화되는데 많은 시간이 소요될 수도 있다. 또한, 독자적으로 많은 리소스를 할당하거나 독립적인 스레드를 띄우는 빈이 있을 수도 있다.

애플리케이션 컨텍스트는 한 번만 생성해서 여러 테스트가 공유해도 문제가 없다. 빈은 기본적으로 싱글톤으로 생성되기 때문에 상태를 갖지 않기 때문이다.

여기서 문제는 JUnit은 테스트 클래스의 오브젝트를 매번 새로 만든다는 점이다. 따라서 스태틱 필드에 애플리케이션 컨텍스트를 저장해두면 된다. JUnit은 @BeforeClass 라는 스태틱 메서드를 지원하는데, 이 메서드에서 애플리케이션 컨텍스트를 만들어 스태틱 변수에 저장해두고 테스트 메서드에서 사용하면 될 것이다. 하지만 이보다 더 편한 방법이 있는데, 바로 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이다.

스프링은 JUnit을 이용하는 테스트 컨텍스트를 제공하는데, 이 테스트 컨텍스트의 지원을 받으면 어노테이션 설정만으로 테스트에 필요한 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유할 수 있다.

UserDaoTest 에서 애플리케이션 컨텍스트를 생성하는 부분을 다음과 같이 수정해준다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DaoFactory.class)
public class UserDaoTest {

  @Autowired
  private ApplicationContext context;

  ...
    
  @Before
  public void setUp() {
    this.dao = context.getBean("userDao", UserDao.class);

    ...
  }
  
  ...
    
}
  • @RunWith - 스프링의 테스트 컨텍스트 프레임워크의 JUnit 확장 기능을 지정한다. 위의 코드에서처럼 SpringJUnit4ClassRunner 라는 JUnit 용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit 이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 해준다.
  • @ContextConfiguration - 테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 위치를 지정한다. xml 파일로 설정하는 경우 locations="" 옵션 사용하면 된다.

setUp() 메서드에서 context 변수의 값을 찍어보면 항상 같은 것을 알 수 있다. 즉, 단 하나의 애플리케이션 컨텍스트가 만들어져 모든 테스트 메서드에서 공유되고 있는 것이다.

하나의 테스트 클래스 안에서뿐만 아니라, 같은 설정파일을 가진 애플리케이션을 사용하는 여러 개의 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유한다.

@Autowired 어노테이션이 붙은 인스턴스 변수에는 애플리케이션 컨텍스트 내에 변수 타입과 일치하는 빈을 찾아서 주입해준다. 스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록하기 때문에 ApplicationContext 타입의 context 변수에 오브젝트가 주입된 것이다.

이제 @Autowired 어노테이션을 이용해서 UserDao 변수를 직접 주입받을 수 있겠다.

  @Autowired
  private UserDao dao;

같은 타입의 빈이 두 개 이상 컨테이너에 올라가 있는 경우, 타입만으로는 어떤 빈을 가져올지 결정할 수 없다. @Autowired 는 타입으로 가져올 빈을 선택할 수 없는 경우 변수의 이름과 같은 이름의 빈이 있는지를 먼저 확인한다. 변수 이름으로도 빈을 찾을 수 없다면 예외가 발생한다.

현재 UserDao 에는 DI 컨테이너가 의존관계 주입에 사용하도록 Setter 메서드가 있다. 이 메서드를 테스트 코드 내에서 호출함으로써 직접 DI할 수도 있다. UserDao가 사용할 DataSource 오브젝트를 테스트 코드 내에서 변경할 수 있는 것이다.

@DirtiesContext
public class UserDaoTest {

  @Autowired
  private UserDao dao;

  @Before
  public void setUp() {
    ...
      
    DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://0.0.0.0:9876/testdb", "spring", "book", true);
    dao.setDataSource(dataSource);
  }
  • @DirtiesContext - 테스트 메서드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 테스트 컨텍스트 프레임워크에 알려준다.

이 방법은 설정파일을 수정하지 않고 오브젝트 관계를 재구성할 수 있다는 장점이 있다. 하지만같은 설정파일을 사용하는 테스트 클래스에서는 애플리케이션 컨텍스트를 공유하므로 주의해야 한다는 단점이 있다.

그래서 @DirtiesContext 어노테이션이 필요하다. 테스트 컨텍스트는 이 어노테이션이 붙은 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 이 어노테이션은 메서드 레벨에서도 사용할 수 있다.

위에서 다룬 방법보다, 테스트용 설정파일을 만들어서 바꿔 끼워주는 방법이 더 좋다.

마지막으로, 스프링 컨테이너를 사용하지 않고 테스트를 만들 수도 있다. 지금 UserDaoTest 는 스프링 DI 컨테이너에 의존적이지 않다. DB에 insert, read 등등만 잘되는지 테스트하면 된다. 따라서 DataSource 를 직접 만들어 UserDao 에 주입만 해주면 된다.

마지막 방법은 애플리케이션 컨텍스트가 필요없기 때문에 테스트 수행 속도가 빠르고 테스트 자체가 간결하다. 따라서 테스트를 위해 필요한 오브젝트의 생성과 초기화가 단순하다면 이 방법을 먼저 고려하는 것이 좋다.

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

학습 테스트learning test란, 개발자 자신이 만들지 않은 프레임워크나 라이브러리 등에 대한 테스트를 말한다. 학습 테스트의 목적은 자신이 사용할 API나 프레임워크의 기능을 테스트해보면서 사용 방법을 익히는 것이다. 테스트를 작성하면서 학습하면 다음과 같은 장점들이 있다.

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

버그 테스트bug test 란, 코드에 오류가 있을 때 그 오류를 가장 잘 드러낼 수 있는 테스트를 말한다. 그래서 버그 테스트는 일단 실패하도록 만들어야 한다. 버그 테스트의 필요성과 장점은 다음과 같다.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions