Chapter2. 테스트

  • Intro
  • UserDaoTest 다시 보기
  • UserDaoTest 개선
  • 개발자를 위한 테스팅 프레임워크 JUnit
  • 스프링 테스트 적용
  • 학습 테스트로 배우는 스프링
  • 정리
  • Review

Intro

Intro

  • 스프링이 개발자에게 제공하는 중요한 가치 중 하나
  • IoC와 DI는 오브젝트의 설계와 생성, 관계, 사용에 관한 기술
  • 지속적으로 변하고 복잡해져가는 어플레케이션에 대응
  • 코드를 확신할 수 있게 해주고, 변화에 대처할 수 있는 자신감을 부여
  • 테스트를 만들지 않는다면 스프링이 지닌 가치의 절반을 포기하는 것
  • 스프링을 학습하는 데 있어 가장 효과적인 방법의 하나

UserDaoTest 다시 보기

테스트의 유용성

  • 테스트용으로 만든 main() 메소드를 반복적으로 실행하며 테스트 가능
  • 코드를 개선하고 수정하는 과정에서 처음과 동일한 기능을 수행함을 보장할 수 있음
  • 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인함으로써 코드를 확신할 수 있게 함

UserDaoTest의 특징

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

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

  • 프레젠테이션 레이어까지 I/O 기능을 만든 후 UI를 통해 테스트
  • DAO를 테스트하기 위해서 관여하는 코드가 너무 많아지고 테스트에 영향을 주게 됨
  • 다른 부분에서 오류가 발생할 수도 있음

작은 단위의 테스트

  • 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트
  • 관심사 분리
  • 한가지 관심사에 집중해서 테스트(단위테스트)
  • 외부 리소스에 의존하는 테스트는 단위 테스트가 아니라고 하는 사람들도 있음
  • 로그인부터 로그아웃까지 다른 단위에 의존하기도 함

테스트 코드의 장점

  • 자동화 될 경우 빠른 테스트 수행이 가능
  • 테스트를 작성해놓음으로써 점진적인 수정과 개선을 할 수 있었음
  • 테스트를 통해 수정중 실수를 찾을 수 있음

UserDaoTest의 문제점

  • 수동 확인 작업의 번거로움
    • 테스트를 작성했으나 여전히 확인하는 작업이 필요
    • 검증해야 하는 양이 많고 복잡해질 수록 불편할 수 밖에 없음
  • 실행 작업의 번거로움
    • 많은 TC를 main에 만들고 그 수만큼 main이 늘어난다면 테스트가 불편해짐

UserDaoTest 개선

테스트 검증의 자동화

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

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

  • main 메소드로는 편리하게 결과를 확인하는데 한계가 있음
  • 어플리케이션 규모가 커지고 테스트 개수가 많아질 수록 부담이 됨
  • JUnit으로 전환함으로써 main메소드를 없애고 오브젝트를 만들어서 실행시키는 코드를 만들지 않는다.
  • 테스트 제어권을 JUnit 프레임워크로 전환

테스트 메소드 전환

import org.junit.Test;
...
public class UserDaoTest {

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

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

검증 코드 전환

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");

    UserDo dao = context.getBean("userDao", UserDao.class);
    User user = new User();
    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

테스트 결과의 일관성

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

  dao.deleteAll();
  assertThat(da.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()));
}

테스트 결과의 일관성

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

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

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

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;
}

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

  • 아직 스프링을 충분히 살펴보지 못했으므로 바로 적용하지 않고 당분간 이 방법을 사용
  • 단위테스트는 항상 일관성 있는 결과가 보장되어야 함
  • DB나 외부 환경에 영향을 받아서는 안됨
  • 테스트를 실행하는 순서에 영향받아서는 안됨

다양한 케이스 테스트

@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));
}

다양한 케이스 테스트

@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");

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

  dao.add(user1);
  dao.add(user2);
  assertThat(da.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()));
}

예외에 대한 테스트

@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");
}

기능설계를 위한 테스트

단계 내용
조건 어떤 조건을 가지고 가져올 사용자 정보가 존재하지 않는 경우에
행위 무엇을 할 때 존재하지 않는 id로 get()을 실행하면
결과 어떤 결과가 나온다 특별한 예외가 던져진다
  • 테스트 코드는 잘 작성된 하나의 기능정의서
  • 기능설계/구현/테스트 라는 일반적인 흐름에서 기능설계에 해당하는 부분 일부를 테스트 코드가 담당
  • 코드로 된 설계문서

테스트 주도 개발

  • 테스트 주도 개발 혹은 테스트 우선 개발 이라고 함
  • 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다 는 것이 TDD의 기본 원칙
  • 코드를 만들고 시간이 많이 지나면 테스트를 만들기가 귀찮아짐
  • 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행
  • 이미 테스트를 만들어 놨기 때문에 테스트를 바로 실행해볼 수 있음
  • 코드에 대한 확신을 가질 수 있음
  • 머릿속으로 하던 테스트를 코드로 표현
  • 화면을 통하지 않고 테스트하기 때문에 디버깅이 빠름

@Before

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 {
    ...
  }
  @Test
  public void count() throws SQLException {
    ...
  }
  @Test(expected=EmptyResultDataAccessException.class)
  public void getUserFailure() throws SQLException {
    ...
  }
}

JUnit LifeCycle

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

스프링 테스트 적용

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

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
  @Autowired
  private ApplicationContext context;
  ...
  @Before
  public void setUp() {
    this.dao = this.context.getBean("userDao", UserDao.class);
    ...
  }
  ...
}
  • @RunWith: JUnit 프레임워크의 테스트 실행 방법을 확장
  • SpringJUnit4ClassRunner: JUnit용 테스트 컨텍스트 프레임워크 확장 클래스, 어플리케이션 컨텍스트를 만들고 관리
  • @ContextConfiguration: 자동으로 만들어줄 어플리케이션 컨텍스트의 설정파일 위치를 지정

DI와 테스트

  • 구현 클래스를 변경할 일이 없더라도 인터페이스를 두고 DI를 적용
    • 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다.
    • 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있음
    • 효율적인 테스트를 손쉽게 만들기 위해서
  • UserDao가 사용할 DataSource 오브젝트를 테스트 코드에서 변경할 수 있음
  • DataSource를 같이 사용 할 경우 운영용 DB를 같이 사용하게됨
  • DI를 이용해서 테스트 중에 DAO가 사용할 DataSource 오브젝트를 바꿔줌

테스트 코드에 의한 DI

@DirtiesContext
public class UserDaoTest {
  @Autowired
  UserDao dao;

  @Before
  public void setUp() {
    ...
    DataSource dataSource = new SingleConnectionDataSource(
      "jdbc:mysql://localhost/testdb", "spring", "book", true
    );
    dao.setDataSource(dataSource);
  }
}
  • @DirtiesContext를 사용해 어플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 알림
  • XML 설정 파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성할 수 있음
  • 미리 정의된 의존관계를 강제로 변경하는 것이기 때문에 주의해서 사용해야 함

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

<bean id="dataSource"
  class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
  <property name="driverClass" value="com.mysql.jdbcDriver" />
  <property name="url" value="jdbc:mysql://localhost/testdb" />
  <property name="username" value="spring" />
  <property name="password" value="book" />
</bean>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-applicationContext.xml")
public class UserDaoTest {
  ...
}

컨테이너 없는 DI 테스트

public class UserDaoTest {
  UserDao dao;

  @Before
  public void setUp() {
    ...
    dao = new UserDao();
    DataSource dataSource = new SingleConnectionDataSource(
      "jdbc:mysql://localhost/testdb", "spring", "book", true
    );
    dao.setDataSource(dataSource);
  }
}
  • DataSource를 직접 만드는 번거로움
  • 컨텍스트를 사용하지 않아 스프링에 의존적이지 않음

DI를 이용한 테스트 방법 선택

  • 컨테이너 없는 방법을 고려
  • 복잡한 의존관계를 갖고있을 경우 DI방식의 테스트를 이용
  • 각각의 환경 차이가 있기 때문에 별도의 설정 파일을 만드는 편이 좋음
  • 강제로 의존관계를 구성할 경우 @DirtiesContext 어노테이션 사용

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

학습 테스트란?

  • 자신이 만들지 않은 프레임워크나 라이브러리 등에 대해서 테스트하는 것
  • API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 목적

학습 테스트의 장점

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

버그 테스트

  • 운영환경 혹은 QA과정에서 발견된 오류를 테스트케이스로 생성
  • 테스트의 완성도를 높여준다.
  • 버그의 내용을 명확하게 분석하게 해준다.
  • 기술적인 문제를 해결하는 데 도움이 된다.

정리

정리

  • 테스트는 자동화돼야 하고, 빠르게 실행할 수 있어야 한다.
  • main() 테스트 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리하다.
  • 테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안 된다.
  • 테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것보다 느쁠 수 있다.
  • 코드 작성과 테스트 수행의 간격이 짧을수록 효과적이다.
  • 테스트하기 쉬운 코드가 좋은 코드다.
  • 테스트를 먼저 만들고 테스트를 성공시키는 코드를 만들어가는 테스트 주도 개발 방법도 유용하다.
  • 테스트 코드도 어플리케이션 코드와 마찬가지로 적절한 리팩토링이 필요하다.

정리

  • @Before, @After를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있다.
  • 스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상시킬 수 있다.
  • 동일한 설정파일을 사용하는 테스트는 하나의 어플리케이션 컨텍스를 공유한다.
  • @Autowired를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI 할 수 있다.
  • 기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하다.
  • 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용하다.

Review

Review

  • 테스트코드로 인해 비즈니스 코드의 설계와 구현에 영향을 주고있음
    • deleteAll(), getCount()
  • 기능정의와 테스트를 연결(BDD)한 부분은 좋지만 좀 더 강조했으면
  • 학습 테스트와 버그 테스트는 매우 유용할 것으로 생각됨
  • 단계별로 변화를 보여주는 부분

References

  1. 이일민 토비의 스프링 3.1 Vol. 1 . 에이콘, pp. 145-207

Chapter2. Test

By Hyeonil Jeong

Chapter2. Test

  • 590