2장. 테스트
2.1 UserDaoTest 다시 보기
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
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 오브젝트)을 직접 코드에서 만들어 넣어준다.
- 테스트의 결과를 콘솔에 출력해준다.
- 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공 메시지로 출력해준다.
▶ 이 테스트 방법에서 가장 돋보이는 건, main() 메소드를 이용해 쉽게 테스트 수행을 가능하게 했다는 점과 테스트할 대상인 UserDao를 직접 호출해서 사용한다는 것이다.
작은 단위의 테스트
- 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다.
- 관심자의 분리라는 원리가 여기에도 적용된다.
- UserDaoTest는 한가지 관심에 집중할 수 있게 작은 단위로 만들어진 테스트다.
- 테스트 수행하기 위해 웹 인터페이스나, 그것을 위한 MVC 클래스, 서비스 오브젝트 등이 필요 없다.
- 서버에 배포할 필요도 없다.
- 이렇게 작은 단위의 코드에 대한 테스트를 수행한 것을 단위 테스트 unit test 라고 한다.
- DB를 이용하기 때문에 단위 테스트가 아닐까 ?? 그렇지 않다. 지금까지 UserDaoTest 를 수행할 때 매번 USER 테이블 내용을 비우고 테스트 진행했다. 만약 DB의 상태가 매번 달라지고, 테스트를 위해 DB를 특정 상태로 만들어주어야 한다면 단위 테스트가 아니라고 보기도 한다.
UserDaoTest의 문제점
- 수동 확인이 번거로움
- 단지 콘솔에 값만 출력하고 있어서 여전히 사람의 눈으로 확인하는 과정이 필요하다.
- 실행 작업이 번거로움
- 간단히 실행 가능한 main() 메소드라 하더라도 매번 하나씩 실행해야 한다.
2.2 UserDaoTest의 개선
- 모든 테스트는 성공과 실패 두 가지 결과를 가질 수 있다.
- 테스트가 진행되는 동안에 에러가 발생해서 실패하는 경우와, (테스트 에러)
- 테스트 작업 중에 에러가 발생하진 않았지만 그 결과가 기대한 것과 다르게 나오는 경우로 구분할 수 있다. (테스트 실패)
테스트의 효율적인 수행과 결과 관리, JUnit
- 자바에는 실용적인 테스트를 위한 도구가 여러 가지 존재한다.
- 그 중에서도 프로그래머를 위한 자바 테스팅 프레임워크라고 불리는 JUnit이 있다.
- 새로 만들 테스트 메소드는 JUnit 가 요구하는 조건 두 가지를 따라야 한다.
- 첫째는 메소드가 public으로 선언돼야 하는 것이고,
- 다른 하나는 메소드에 @Test 라는 애노테이션을 붙여주는 것이다.
- JUnit에서 제공하는 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");
}
}
2.3 개발자를 위한 테스팅 프레임워크 JUnit
테스트 결과의 일관성
- UserDaoTest에서 생각해 볼 문제는 테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다는 점이다.
- 반복적으로 테스트 했을 때 테스트가 실패하기도 하고 성공하기도 한다면 이는 좋은 테스트라고 할 수 없다.
- UserDaoTest의 문제는 이전 테스트 때문에 DB에 등록된 중복 데이터가 있을 수 있다는 점이다.
- 가장 좋은 해결책은 addAndGet() 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서, 테스트를 수행하기 이전 상태로 만들어주는 것이다.
- 단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다는 점을 잊어선 안된다.
- DB에 남아있는 데이터와 같은 외부 환경에 영향을 받지 말아야 하는 것은 물론이고, 테스트 실행하는 순서 바꿔도 동일한 결과가 보장되도록 만들어야 한다.
동일한 결과를 보장하는 테스트
- 테스트 시작하기 전에 deleteAll() 통해 안에 데이터 전체 제거한다.
- getCount() 통해 갯수가 0인 것을 확인하고 진행한다.
@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()));
}
- 또 다른 방법으로는 addAndGet() 테스트를 마치기 직전에 테스트가 변경하거나 추가한 데이터를 모두 원래 상태로 만들어 주는 것이다.
- 하지만 addAndGet() 테스트 실행 전에 다른 이유로 USER 테이블에 데이터가 들어가 있다면 이때 테스트가 실패할 수 있다.
get() 예외조건에 대한 테스트
- 하나는 null과 같은 특별한 값을 리턴하는 것이고, 다른 하나는 id에 해당하는 정보를 찾을 수 없다고 예외를 던지는 것이다.
- @Test 에노테이션의 expected 엘리먼트 통해 실행 중에 발생하리라 기대되는 예외 클래스를 넣어준다.

- 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다.
- get() 메소드의 경우라면, 존재하는 id가 주어졌을 때 해당 레코드를 정확히 가져오는가를 테스트하는 것도 중요하지만, 존재하지 않는 id가 주어졌을 때 어떻게 반응할지 결정하는 것도 좋다.
테스트 주도 개발
- 만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 케스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법을 테스트 주도 개발 (TDD, Test Driven Development)라고 한다.
- 혹은 테스트를 먼저 개발한다고 해서 TFD (Test First Development) 라고 한다.
테스트 코드 개선
- 중복된 코드를 setUp() 이라는 함수에 옮겨두고 이를 @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");
}
@Test
public void andAndGet() throws SQLException {
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()));
}
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.get("unknown_id");
}
- JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.
- 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
- @Before가 붙은 메소드가 있으면 실행한다.
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
- @After가 붙은 메소드가 있으면 실행한다.
- 나머지 테스트 메소드에 대해 2~5번을 반복한다.
- 모든 테스트의 결과를 종합해서 돌려준다.
- @Before나 @After 메소드를 테스트 메소드에서 직접 호출하지 않기 때문에 서로 주고받을 정보나 오브젝트가 있다면 인스턴스 변수를 이용해야 한다.
- 또 한가지 기억해야 할 점은 각 테스트 메소드 실행할때마다 테스트 클래스의 오브젝트를 새로 만든다는 점이다. 한 번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다.
- 이렇게 하는 이유는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해서이다.
- 메소드의 일부에서만 공통적으로 사용되는 코드가 있다면, @Before를 사용하기 보다는, 일반적인 메소드를 분리하고 테스트 메소드에서 직접 호출해 사용하도록 만드는 편이 낫다.

픽스쳐 fixture
- 테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스쳐라고 한다.
- 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이용해 생성해두면 편리하다.
- UserDatoTest에서 dao가 대표적인 픽스쳐이다.
- user1, user2, user3 세 가지 인스턴스 변수를 선언하고 오브젝트 생성은 @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");
}
2.4 스프링 테스트 적용
- 한 가지 남은 문제는 애플리케이션 컨텍스트 생성 방식이다.
- @Before 메소드가 테스트 메소드 개수만큼 반복되기 때문에 애플리케이션 컨텍스트도 세 번 만들어진다.
- 애플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화한다. 이 중 어떤 빈은 오브젝트가 생성될 때 자체적인 초기화 작업을 진행하기 때문에 시간이 많이 소요될 수 있다.
- 애플리케이션 컨텍스트가 초기화될 때 어띤 빈은 독자적으로 많은 리소스 할당하거나 독립적인 스레드 뛰우는데 이때 테스트 마칠때 정리해주지 않으면 새로운 문제가 발생할 수 있다.
- 애플리케이션 컨텍스트는 초기화되고 나면 내부의 상태가 바뀌는 일은 거의 없다.
- UserDao 빈을 가져다가 add(), get() 함수 등을 사용한다고 해서 UserDao의 상태가 바뀌진 않는다.
- Junit 테스트에서 컨텍스트 테스트 지원 기능을 사용하는 것이 더 편리하다.
2.4.1 테스트를 위한 애플리케이션 컨텍스트 관리
- 스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다.
- ApplicationContext 타입의 인스턴스 변수 선언하고 @Autowired 애노테이션 붙여준다.
- 클래스 레벨에 @Runwith와 @ContextConfiguration 애노테이션을 추가해준다.

테스트 메소드의 컨텍스트 공유
- context 모두 세 번 동일하게 사용된 것을 알 수 있다.
- UserDao는 매번 주소값이 다르다.


@Autowired
- @Autowired가 붙은 인스턴스 변수가 있다면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 변수를 찾는다.
- 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다.
2.4.2 DI와 테스트
- 우리는 절대로 DataSource의 구현 클래스를 바꾸지 않을 것이다. 그럼에도 DI를 통해 주입해주는 방식을 이용해야 하는가 ? "그래도 인터페이스를 두고 DI를 적용해야 한다"
- 소프트웨어 개발에서 절대로 바뀌는 것은 없기 때문이다.
- 클래스의 구현 방식은 바뀌지 않더라도 인터페이스를 두고 DI를 적용하게 하면, 다른 차원의 서비스 기능을 도입할 수 있다.
- 효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야 한다.
- 가능한 작은 단위의 대상에 국한해서 테스트해야 하기 때문이다.
테스트를 위한 수동 DI를 적용한 UserDaoTest

테스트를 위한 별도의 DI 설정
- 기존의 applicationContext.xml 을 복사해서 test-applicationContext.xml 을 만든다.


스프링 컨테이너 없는 DI 설정

- UserDao가 스프링의 API에 의존하지 않고 자신의 관심에만 만들어졌기 때문에 가능했다.
DI를 이용한 테스트 방법의 선택
- 항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 우선적으로 고려한자. 이 방법이 테스트 수행 속도가 가장 빠르고 간결하다.
- 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트 테스트해야 하는 경우 스프링의 설정을 이용한 DI 방식의 테스트 이용하면 편리하다.
- 테스트에서 애플리케이션 컨텍스르를 사용하는 경우에는 테스트 전용 설정파일을 따로 만드는 것이 좋다.
- 참고로, 테스트 설정을 따로 만들었더라도 예외적인 의존관계를 강제로 구성해야 하는 경우,
- 컨텍스트에서 DI 받은 오브젝트에 다시 테스트 코드로 수동 DI해서 테스트 하는 방법을 사용하면 된다.
- @DirtiesContext 애노테이션을 붙여야 한다.
2.5 학습 테스트로 배우는 스프링
- 자신이 만들지 않는 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서도 테스트를 작성한다.
- 이런 테스트를 학습 테스트 learning test 라고도 한다.
2.5.1 학습 테스트의 장점
- 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
- 학습 테스트 코드를 개발 중에 참고할 수 있다.
- 수동으로 예제를 만들 때는 코드를 계속 수정하면서 기능을 확인해보지만,
- 학습 테스트는 다양한 기능과 조건에 대한 테스트 코드를 개별적으로 남겨둘 수 있다.
- 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
- 테스트 작성에 대한 좋은 훈련이 된다.
- 새로운 기술을 공부하는 과정이 즐거워진다.
2.5.2 학습 테스트 예제
JUnitTest 테스트 오브젝트 와 스프링 테스트 컨텍스트
@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;
}
}
- 테스트 메소드가 실행될 때마다 스태틱 변수인 testObject에 저장해둔 오브젝트와 다른 새로운 오브젝트가 만들어졌음을 확인할 수 있다.
- 하지만 context는 동일한 ApplicationContext 객체를 공유하게 된다.
- SpringJUnit4ClassRunner를 사용하여 테스트를 실행하면, Spring 컨테이너는 한 번만 로드
- 따라서 ApplicationContext는 모든 테스트 메소드에서 동일한 객체이다.
- contextObject가 null이거나 (contextObject == null), 기존 contextObject와 같은 객체인지 (contextObject == this.context) 확인하는 부분에서 항상 true가 나온다.
2.5.3 버그 테스트
- 버그 테스트란 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말한다.
- 버그 테스트는 일단 실패하도록 만들어야 한다.
- 그리고 나서 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다.
버그 테스트의 필요성
- 테스트의 완성도를 높여준다.
- 버그의 내용을 명확하게 분석하게 해준다.
- 기술적인 문제를 해결하는 데 도움이 된다.
2.6 정리
- 테스트는 자동화돼야 하고, 빠르게 실행할 수 있어야 한다.
- main() 테스트 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리하다.
- 테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안 된다.
- 테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것보다 나쁠 수 있다.
- 코드 작성과 테스트 수행의 간격이 짧을수록 효과적이다.
- 테스트하기 쉬운 코드가 좋은 코드다.
- 테스트를 먼저 만들고 테스트를 성공시키는 코드를 만들어가는 테스트 주도 개발 방법도 유용하다.
- 테스트 코드도 애플리케이션 코드와 마찬가지로 적절한 리팩토링이 필요하다.
- @Before, @After를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있따.
- 스프링 테스트 컨텍스트 프레임워크를 사용하면 테스트 성능을 향상시킬 수 있다.
- 동일한 설정파일을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 공유한다.
- @Autowired를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI할 수 있다.
- 기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하자.
- 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용하다.
토비 스프링 소스코드 ▶ https://github.com/AcornPublishing/toby-spring3-1/tree/main
GitHub - AcornPublishing/toby-spring3-1: 토비의 스프링 3.1
토비의 스프링 3.1. Contribute to AcornPublishing/toby-spring3-1 development by creating an account on GitHub.
github.com
토비 스프링 ▶ https://product.kyobobook.co.kr/detail/S000000935358
토비의 스프링 3.1 Vol 1: 스프링의 이해와 원리 | 이일민 - 교보문고
토비의 스프링 3.1 Vol 1: 스프링의 이해와 원리 | 대한민국 전자정부 표준 프레임워크 스프링을 설명하는 No. 1 베스트셀러! 단순한 예제를 스프링 3.0과 스프링 3.1의 기술을 적용하며 발전시켜 나
product.kyobobook.co.kr
'스터디 > [토비의 스프링 3.1 Vol 1] (2025.03)' 카테고리의 다른 글
[토비의 스프링 3.1 Vol 1] 5장. 서비스 추상화 (~5.2.2) (0) | 2025.04.06 |
---|---|
[토비의 스프링 3.1 Vol 1] 4장. 예외 처리 (0) | 2025.03.29 |
[토비의 스프링 3.1 Vol 1] 3장. 템플릿 (0) | 2025.03.23 |
[토비의 스프링 3.1 Vol 1] 1장. 오브젝트와 의존관계 (1) | 2025.03.09 |