스터디/[토비의 스프링 3.1 Vol 1] (2025.03)

[토비의 스프링 3.1 Vol 1] 2장. 테스트

ttoance 2025. 3. 16. 09:00
반응형

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

 

반응형