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
- 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
- @Before가 붙은 메소드가 있으면 실행한다.
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
- @After가 붙은 메소드가 있으면 실행한다.
- 나머지 테스트 메소드에 대해 2~5번을 반복한다.
- 모든 테스트의 결과를 종합해서 돌려준다.
스프링 테스트 적용
스프링 테스트 컨텍스트 프레임워크
@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
- 이일민 토비의 스프링 3.1 Vol. 1 . 에이콘, pp. 145-207
Chapter2. Test
By Hyeonil Jeong
Chapter2. Test
- 590