Table of Contents

개요#

1장은 스프링이 어떤것이고 무엇을 제공하는지보다는 스프링이 관심을 갖는 대상인 오브젝트의 설계와 구현, 동작원리에 더 집중하기를 바란다.

2.1. UserDaoTest 다시보기#

2.1.1. 테스트의 유용성#

테스트란 예상한대로 코드가 정확히 동작하는지 확인해서 만든 코드를 확신할수 있게 해주는 작업

2.1.2. UserDaoTest의 특징#

package springbook.user.dao;

import java.sql.SQLException;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import springbook.user.domain.User;

public class UserDaoTest {
	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
		UserDao dao = context.getBean("userDao", UserDao.class);
		
		User user = new User();
		user.setId("whiteship");
		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의 오브젝트를 가져와 메소드를 호출한다.
  • 테스트에 사용할 입력 값(User오브젝트)을 직접 코드에서 만들어 넣어준다.
  • 테스트의 결과를 콘솔에 출력해준다.
  • 각 단계의 작업이 에러없이 끝나면 콘솔에 성공 메시지를 출력해준다.

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

  • BO, Action클래스까지 모두 만들고 나서 웹페이지에 폼을 만들어서 실제 구동 후 화면 입력값을 받아서 기능을 확인한다.
  • 가장 흔히 사용하는 방법이지만 모든 레이어를 모두 구현하고 테스트함에 따라 단점이 있다.
    • 먼저 우리가 하고자 하는건 DAO에 대한 테스트라는데 기억하자.
    • 문제 발생시 어느 시점의 어느 코드가 문제인지 파악하는게 힘들다.
    • 코드가 아닌 다른 설정상의 문제일수도 있다.
    • 즉 DAO 테스트를 위해 전체 레이어를 개발하고 사용하는 것은 번거롭고 오류에 대한 빠른 대응이 어렵다.

작은 단위의 테스트#

  • 테스트 하고자 하는 그 대상에만 집중하는게 좋다.
  • 통합테스트만을 하는 것보다는 단위테스트를 통해 개별단위의 작은 기능을 확인하면 통합테스트시 문제가 발생하더라도 조금더 쉽게 해결할 수 있다.

자동수행 테스트 코드#

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

2.1.3. UserDaoTest 의 문제점#

  1. 수동 확인 작업의 어려움
    • 로그를 출력하는 형태라 결과를 눈으로 확인하고 정상여부를 판단해야 한다.
  2. 실행작업의 번거로움
    • main() 메소드를 가진 클래스가 많을수록 하나하나 수동으로 실행하기에는 번거롭다.

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

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

JUnit 테스트로 전환#

테스트 메소드 전환#

  • JUnit 프레임워크가 요구하는 조건 두가지
    1. 메소드가 public
    2. 메소드에 @Test 애노테이션 추가

검증코드 전환#

package springbook.user.dao;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

.....

public class UserDaoTest {
	
	@Test 
	public void andAndGet() throws SQLException {
		ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
		UserDao 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()));
	}
	
	public static void main(String[] args) {
		JUnitCore.main("springbook.user.dao.UserDaoTest");
	}
}

JUnit 테스트 실행#

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

2.3.1 JUnit 테스트 실행방법#

IDE#

빌드툴#

2.3.2. 테스트 결과의 일관성#

반복적으로 테스트를 했을때 실패하기도 하고 성공하기도 하면 좋은 테스트아니다. 반복적으로 테스트를 진행하더라도 항상 일관적인 결과가 나오는게 좋다. 앞서 UserDaoTest의 경우 데이터를 입력 후 조회하는 형태라 다시 실행하면 중복키 에러가 발생한다.

deleteAll()과 getCount() 추가#

  • add()와 get() 메소드만으로 구성할 경우 테스트를 돌릴때마다 매번 앞서 넣었던 데이터를 수동으로 지워야 한다.
  • 매번 수동으로 지우기 보다는 deleteAll()와 getCount() 메소드를 추가해보자.
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;
}

deleteAll()과 getCount() 의 테스트#

  • 테스트 실행시 가장 먼저 deleteAll() 메소드를 실행해서 데이터를 지운다.
  • 데이터를 모두 지우거나 추가하면서 getCount() 메소드를 사용해서 데이터 갯수를 확인한다.
@Test 
public void andAndGet() throws SQLException {
	ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
	UserDao dao = context.getBean("userDao", UserDao.class);
	
	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()));
}

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

  • add()와 get()로만 구성할 경우 매번 수동으로 지워주지 않으면 테스트 결과가 다르다.
  • deleteAll()과 getCount()를 적절히 사용하면 테스트는 매번 동일한 결과를 보장한다.

2.3.3. 포괄적인 테스트#

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

addAndGet() 테스트 보완#

@Test 
public void andAndGet() 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");
	
	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() 예외조건에 대한 테스트#

  • 테스트 실행시 특정 예외가 발생할 경우 @Test애노테이션에서 expected 속성에 해당되는 예외 클래스를 적어준다.
  • expected 속성을 사용할 경우 해당되는 예외가 발생하면 성공 예외가 발생하지 않으면 실패한다.
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
	dao.deleteAll();
	assertThat(dao.getCount(), is(0));
	
	dao.get("unknown_id");
}

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

  • 앞서 해당되는 id의 사용자가 없다면 예외를 던질때 테스트가 성공하도록 했다.
  • 하지만 get() 메소드는 예외를 던지는 코드가 없다.
  • 해당되는 id의 사용자가 없으면 예외를 던져야 테스트가 성공하니까 다음처럼 예외를 던지도록 코드를 수정한다.
public User get(String id) throws SQLException {
	.....
	
	ResultSet rs = ps.executeQuery();

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

포괄적인 테스트#

  • 개발자는 대개 성공하는 테스트만 골라서 작성하는 경우가 많다.
  • 항상 네거티브 테스트를 먼저 만들자 (스프링의 창시자 로드존슨)

2.3.4. 테스트가 이끄는 개발#

  • 앞서 코드는 기능을 만들고 테스트를 만든게 아니라 테스트를 먼저 만들고 기능을 넣었다.

기능설계를 위한 테스트#

단계 내용 코드
조건 어떤 조건을 가지고 가져올 사용자 정보가 존재하지 않는 경우에 daodeleteAll()
assertThat(dao.getCount(), is(0));

테스트 주도 개발#

  • 테스트 주도 개발(TDD, Test Driven Development)
  • 테스트 우선 개발(Test First Development)

2.3.5. 테스트 코드 개선 #

  • 테스트 코드를 리팩토링 해보자.
  • 테스트 코드에서 중복을 제거하자.

@Before#

@Before
public void setUp() {
	ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
	this.dao = context.getBean("userDao", UserDao.class);
	
	this.user1 = new User("gyumee", "박성철", "springno1");
	this.user2 = new User("leegw700", "이길원", "springno2");
	this.user3 = new User("bumjin", "박범진", "springno3");
}
  • JUnit 은 다음과 같은 형태로 동작한다. 프레임워크를 사용할때는 그 프레임워크가 어떻게 동작하는지 알아두는게 좋다.
    1. 테스트 클래스에서 @Test가 붙은 public 이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
    2. 테스트 클래스의 오브젝트를 하나 만든다.
    3. @Before 가 붙은 메소드를 있으면 실행한다.
    4. @Test 가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
    5. @After 가 붙은 메소드가 있으면 실행한다.
    6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
    7. 모든 테스트의 결과를 종합해서 돌려준다.
그림 2-4

픽스처#

  • 픽스처 : 테스트를 수행하는데 필요한 정보나 오브젝트(UserDaoTest 에서는 dao가 대표적인 픽스처)
  • 일반적으로 픽스처는 여러 테스트에서 사용하기 때문에 @Before 메소드에서 생성해두면 편리하다.
public class UserDaoTest {
	private UserDao dao; 
	
	private User user1;
	private User user2;
	private User user3;
	
	@Before
	public void setUp() {
		.....
		
		this.user1 = new User("gyumee", "박성철", "springno1");
		this.user2 = new User("leegw700", "이길원", "springno2");
		this.user3 = new User("bumjin", "박범진", "springno3");

	}

2.4. 스프링 테스트 적용#

2.4.1. 테스트를 위한 애플리케이션 컨텍스트 관리#

스프링 테스트 컨텍스트 프레임워크 적용#

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
	@Autowired
	ApplicationContext context;
	
	.....
	
	@Before
	public void setUp() {
		this.dao = this.context.getBean("userDao", UserDao.class);

		.....
	}

  • @ContextConfiguration 애노테이션은 ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml"); 를 대체한다.
  • @RunWith 는 JUnit 프레임워크의 테스트 실행방법을 확장할때 사용하는 애노테이션이다. SpringJUnit4ClassRunner 라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정하면 테스트 중에 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.

테스트 메소드의 컨텍스트 공유#

@Before
public void setUp() {
	System.out.printlnl(this.context);
	System.out.println(this);

org.springframework.conext.support.GenericApplicationContext@d3d6f:
springbook.dao.UserDaoTest@115d06c
org.springframework.conext.support.GenericApplicationContext@d3d6f:
springbook.dao.UserDaoTest@116318b
org.springframework.conext.support.GenericApplicationContext@d3d6f:
springbook.dao.UserDaoTest@15e0c2b
  • UserDaoTest 의 주소값은 매번 다르다. 즉 테스트를 실행할때마다 새로운 객체를 생성한다.
  • 스프링 테스트 컨텍스트가 제공하는 기능으로 Context의 주소값은 같다. 내부적으로 DI처럼 테스트를 실행할때마다 미리 만들어둔 컨텍스트를 주입해준다.
  • 컨텍스트를 매번 생성하지 않아서 여러개의 테스트 실행시 테스트를 빨리 끝난다.

테스트 클래스의 컨텍스트 공유#

  • @ContextConfiguration 의 설정파일이 동일하면 여러개의 테스트 클래스간에도 컨텍스트를 공유하다.

@Autowired#

  • @Autowired 가 붙은 필드에는 해당되는 타입의 빈을 자동으로 주입해서 설정한다.
  • ApplicationContext 도 DI가 가능하다.

2.4.2. DI와 테스트#

  • UserDao는 DB커넥션을 생성하기 위해 DataSource 인터페이스를 사용한다.
  • 하지만 DataSource가 아니라 SimpleDriverDataSource 를 사용한다면 ? 그래도 인터페이스를 두고 DI를 적용해야 하는 이유는?
    1. 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다. 향후 변경을 고려해 인터페이스를 사용하고 DI를 적용하는 것이 좋다.
    2. 클래스의 구현방식은 바뀌지 않는다 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있다.
    3. 효율적인 테스트를 하기 위해 필요하다.

테스트 코드에 의한 DI#

  • UserDao는 DataSource를 주입하기 위한 수정자 메소드를 가진다.
  • 테스트시 테스트를 쉽게 할수 있도록 별도의 DataSource를 설정할 수 있다.
    • 테스트시 운영용 DB 커넥션을 설정하면 안된다.
    • 그래서 테스트할때는 테스트를 위한 DataSource를 설정하자.
.....
@DirtiesContext
public class UserDaoTest {
	@Autowired
	UserDao dao; 
		
	@Before
	public void setUp() {
		..... 
		DataSource dataSource = new SingleConnectionDataSource(
			"", "", "", true);
		dao.setDataSource(dataSource);
		.....

	}
  • @DirtiesContext 는 테스트 메소드에서 애플리케이션 컨텍스트 구성이나 상태를 변경한다는 것을 테스트 컨텍스트 프레임워크에 알려준다. 스프링 테스트 컨텍스트를 사용할 경우 전체 테스트 클래스가 컨텍스트 정보를 공유한다. 그래서 위 코드처럼 특정 클래스에서 빈의 상태를 변경할 경우 전체에 영향을 줄수 있다. 해당되는 테스트 클래스에만 컨텍스트 정보를 다시 만들도록 하기 위해 사용한다.
  • 코드를 사용해서 직접 DI를 수행한다. (setDataSource())
  • 이렇게 처리할 경우 설정파일(applicationContext.xml)을 수정하지 않고도 오브젝트를 재구성할 수 있다.

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

  • 앞서 본것처럼 특정 테스트 클래스에서 설정할 경우 매번 변경해야 하는 부담이 있다.
  • 이런 경우 테스트용 설정파일을 사용하는게 좋다. 예를들면 test-applicationContext.xml

컨테이너 없는 DI 테스트#

public class UserDaoTest {
	UserDao dao;

	.....

	@Before
	public void setUp() {
		.....
		dao = new UserDao();
		DataSource dataSource = new SingleConnectionDataSource(
			"", "", "", true);
		dao.setDataSource(dataSource);

	}

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

  • 학습테스트 : 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서 테스트를 작성하는 행위
  • 학습테스트으 목적
    1. 자신이 사용할 API나 프레임워크의 기능을 테스트해보면서 사용방법을 익힌다.
    2. 대부분은 해당 프레임워크의 기능검증을 목적으로 하지는 않는다.

2.5.1 학습테스트의 장점#

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

2.5.2. 학습테스트 예제#

JUnit 테스트 오브젝트 테스트#

스프링 테스트 컨텍스트 테스트#

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("junit.xml")
public class JUnitTest {
	@Autowired ApplicationContext context;
	
	static Set<JUnitTest> testObjects = new HashSet<JUnitTest>();
	static ApplicationContext contextObject = null;
	
	@Test public void test1() {
		assertThat(testObjects, not(hasItem(this)));
		testObjects.add(this);
		
		assertThat(contextObject == null || contextObject == this.context, is(true));
		contextObject = this.context;
	}
	
	@Test public void test2() {
		assertThat(testObjects, not(hasItem(this)));
		testObjects.add(this);
		
		assertTrue(contextObject == null || contextObject == this.context);
		contextObject = this.context;
	}
	
	@Test public void test3() {
		assertThat(testObjects, not(hasItem(this)));
		testObjects.add(this);
		
		assertThat(contextObject, either(is(nullValue())).or(is(this.contextObject)));
		contextObject = this.context;
	}
}

2.5.3. 버그 테스트#

  • 버그테스트 : 코드에 오류가 있을때 그 오류를 가장 잘 드러내줄 수 있는 테스트
  • 버그테스트의 필요성과 장점
    1. 테스트의 완성도를 높여준다.
    2. 버그의 내용을 명확하게 분석하게 해준다.
    3. 기술적인 문제를 해결하는데 도움이 된다.

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-6) was last changed on 26-Oct-2014 12:33 by DongGukLee