Spring

토비의 Spring_Chapter02_테스트

강용민 2022. 10. 14. 00:56

애플리케이션은 계속 변하고 복잡해져 간다.

그 변화에 대응하는 첫 번째 전략이 확장과 변화를 고려한 객체지향적 설계와 그것을 효과적으로 담아낼 수 있는 IoC/DI 같은 기술이라면, 두 번째 전략은 코드의 확신과 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술이다.

 

2.1 UserDaoTest 다시보기

테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 확신할 수 있게 해주는 작업임과 동시에 코드나 설계에 결함을 확인할 수 있다.

 

UserDaoTest의 특징

UserDaoTest의 특징은 다음과 같다.

  • main() 메소드를 이용한다.
  • 테스트할 대상인 UserDao의 오브젝트를 가져와 메소드를 호출한다.
  • 테스트에 사용할 입력 값(User 오브젝트)을 직접 코드에 만들어 넣어준다.
  • 테스트의 결과를 콘솔에 출력한다.
  • 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공 메시지를 출력한다.

 

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

보통 웹 프로그램에서 사용하는 DAO를 테스트하는 방법은 다음과 같다.

DAO를 만든 뒤 바로 테스트하지 않고, 서비스 계층, MVC 프레젠테이션 계층까지 포함한 모든 입출력 기능을 대충이라도 코드로 다 만든뒤 테스트한다.

하지만 이 방법은 테스트하고 싶었던 건 UserDao였는데 다른 계층의 모든 것까지 다 만들어야 하며, 오류가 있을 때 빠르고 정확하게 대응하기 힘들다는 단점이 있다.

 

작은 단위 테스트

테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다.

따라서 테스트는 가능하면 작은 단위로 쪼개서 집중할 수 있어야 하는데 이러한 테스트를 단위 테스트(Unit Test)라 한다.

UserDaoTest는 UserDao라는 작은 단위의 데이터 액세스 기능만을 테스트하기 위해 만들어졌고, 그 외의 계층이 참여하지 않기에 이는 분명 단위 테스트이다.

단위 테스트가 필요한 또 다른 이유는 다음과 같다.

  • 떄로는 단위 테스트 없이 긴 테스트만 하는 경우도 있는데, 각 단위별로 테스트를 먼저 모두 진행하고 나서 긴 테스트를 시작했다면 훨씬 나을 것이다.
  • 전문 테스터나 고객이 직접 단위 테스트를 하는 경우도 있는데 이때는 단위가 제법 커질 것이다.그때서야 오류가 처음 발견되고 수정해야한다면 오래전에 만든 코드를 뒤져서 버그를 수정해야 한다.
    이런 경우와 코드를 만들자마자 빠르게 테스트해서 오류를 발견했을 때 버그를 수정하는 경우를 비교해보자.

 

자동수행 테스트 코드

UserDaoTest의 한 가지 특징은 테스트할 데이터가 코드를 통해 제공되고, 테스트 작업 역시 코드를 통해 자동으로 실행된다는 점이다.

자동으로 수행되는 테스트의 장점은 자주 반복할 수 있다는 것이다. 번거로운 작업이 없고 테스트를 빠르게 실해할 수 있기 때문에 언제든 코드를 수정하고 나서 테스트를 해볼 수 있다.

그런데 애플리케이션을 구성하는 클래스 안에 테스트 코드를 포함시키는 것보다는 별도로 테스트용 클래스를 만들어 테스트 코드를 넣는 편이 낫다.

 

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

그렇게 작은 단계를 거치는 동안 테스트를 수행해서 확신을 가지고 코드를 변경해갔기 때문에 전체적으로 코드를 개선하는 작업에 속도가 붙고 더 쉬워졌을 수도 있다.

또 UserDao의 기능을 추가하려고 할 때도 미리 만들어둔 테스트 코드는 유용하게 쓰일 수 있다. 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수도 있다.

 

UserDaoTest의 문제점

UserDaoTest가 수동 테스트에 비해 장점이 많은 건 사실이나 아쉬운 부분이 존재한다.

  • 수동 확인 작업의 번거로움
    • UserDaoTest는 자동으로 진행하도록 만들어졌으나 결과 값와 예상값이 일치하는지를 테스트 코드는 확인해주지 않는다. 성공적으로 되고 있는지를 확인하는 건 사람의 책임이다.
  • 실행 작업의 번거로움
    • 아무리 간단히 실행 가능한 main() 메소드라고 하더라도 매번 그것을 실행하는 것은 번거롭다.

그래서 main() 메소드를 이요하는 방법보다 좀 더 편리하고 체계적으로 테스트를 실행하고 그 결과를 확인하는 방법이 절실히 필요하다.

 

2.2 UserDaoTest 개선

UserDaoTest의 두 가지 문제점을 개선해보자.

 

테스트 검증의 자동화

UserDaoTest를 통해 확인하고 싶은 사항은, add()에 전달한 User 오브젝트에 담긴 사용자 정보와 get()을 통해 다시 DB에서 가져온 User 오브젝트의 정보가 서로 정확히 일치하는가이다.

테스트 에러는 콘솔 에러 메세지로 쉽게 확인 가능하지만, 결과가 의도와 다른 테스트 실패는 별도의 확인 작업이 필요하다. 기존 테스트를 조금 더 명확하게 수정한 코드는 다음과 같다.

 

UserDaoTest.java

package springbook.user.dao;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import springbook.user.domain.User;

import java.sql.SQLException;

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("user");
        user.setName("백기선");
        user.setPassword("married");

        dao.add(user);

        System.out.println(user.getId() + "등록 성공");

        User user2 = dao.get(user.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을 이용해 다시 작성해보겠다.

JUnit도 프레임워크라 IoC를 따르고 있어 클래스의 오브젝트를 생성하고 실행하는 일은 프레임워크에 의해 진행된다.

 

테스트 메소드 전환

테스트가 main() 메소드로 만들어졌다는 건 제어권을 직접 갖는다느 의미이기에 일반 메소드로 옮긴다.

새로 만들 테스트 메소드는 JUnit 프레임워크가 요구하는 조건 두 가지를 따라야 한다.

첫째는 메소드가 public으로 선언돼야 하는 것이고, 다른 하나는 메소드에 @Test라는 애노테이션을 붙여주는 것이다.

 

public class UserDaoTest {
    @Test
    public void addAndGet() throws SQLException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

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

 

검증 코드 전환

테스트의 결과를 검증하는 if/else 문장을 JUnit이 제공하는 방법을 이용해 전환해보자.

assertThat() 메소드는 첫 번째 파라미터의 값을 뒤에 나오는 매처(matcher)라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어준다. is()는 매처의 일종으로 equals() 로 비교해주는 기능을 가졌다.

package springbook.user.dao;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import springbook.user.domain.User;

import java.sql.SQLException;

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

public class UserDaoTest {
    @Test
    public void addAndGet() throws SQLException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

        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());
        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user2.getPassword()));
    }
}

 

JUnit 테스트 실행

JUnit 프레임워크를 이용해 앞에서 만든 테스트 메소드를 실행하도록 코드를 만들어 보자.

package springbook.user.dao;

//생략

public class UserDaoTest {
    @Test
    public void addAndGet() throws SQLException {
        // 생략
    }

    public static void main(String[] args){
        JUnitCore.main("springbook.user.dao.UserDaoTest");
    }
}

 

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

스프링의 핵심 기능 중 하나인 스프링 테스트 모듈도 JUnit을 이용한다.

 

JUnit 테스트 실행 방법

 JUnitCore를 통해 콘솔로 출력결과를 보는 방법도 있지만 자바 IDE(이클립스)에 내장된 JUnit 테스트 지원 도구 사용이 더 유용하다. 이를 사용하면 JUnitCore을 사용할 때처럼 main()을 만들 필요가 없다. 

또한 테스트는 한 번에 여러 테스트 클래스를 동시에 실행 가능하다. 특정 패키지를 선택 후 JUnit Test를 실행시키면 패키지 아래의 모든 JUnit 테스트를 한 번에 실행시켜준다.

 

테스트 결과의 일관성

지금까지 테스트를 실행하면서 가장 불편했던 일은, 매번 UserDaoTest 테스트를 실행하기 전에 DB의 USER 테이블 데이터를 모두 삭제해줘야 할 때엿다.

가장 좋은 해결책은 addAndGet() 테스트를 마치고 나면 테스트를 수행하기 이전 상태로 만들어주는 것이다.

 

deleteAll()의 getCount()추가

  • deleteAll()메소드로 USER 테이블의 모든 레코드를 삭제하주는 기능이다.
  • getCount()메소드로, USER 테이블의 레코드 개수를 돌려준다.

UserDaoTest.java

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() 메소드는 독립적으로 자동 시랭되는 테스트를 만들기 힘들어 기존에 만든 addAndGet() 테스트를 확장하는 방법을 사용하는 편이 더 나을 것이다.

 

UserDaoTest.java

@Test
public void addAndGet() throws SQLException, ClassNotFoundException{
    //XML설정파일을 사용한 애플리케이션컨텍스트 초기화
    ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
				
    UserDao dao = context.getBean("userDao",UserDao.class);
		
    dao.deleteAll();	//최초에 데이터 초기화
    assertThat(dao.getCount(),is(0));	//데이터 개수가 0인지 확인
		
    //테스트용 데이터 셋팅
    User user = new User();
    user.setId("gyumee");
    user.setName("박성철");
    user.setPassword("springno1");
		
    //dao 함수 호출
    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()));	
}

이제 DB 데이터를 지우지 않아도 항상 성공하는 테스트가 만들어졌다. 단위 테스트는 항상 일관성 있는 결과가 보장되어야 한다. DB에 남아있는 데이터나 외부 환경에 영향을 받아서는 안되며 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되어야 한다.

 

포괄적인 테스트

미처 생각하지 못한 문제가 숨어 있을지도 모르니 더 꼼꼼한 테스트를 해보는 것이 좋은 자세다.

 

getCount()테스트

이번에는 여러 개의 User를 등록해가면서 getCOunt()의 결과르 ㄹ매번 확인해보겠다.

JUnit은 하나의 클래스 안에 여러 개의 테스트 메소드가 들어가는 것을 허용한다. @Test가 붙어 있고 public 접근자가 있으며 리턴 값이 void형이고 파라미터가 없다는 조건을 지키기만 하면 된다.

User 오브젝트를 여러 번 만들고 값을 넣어야 하니, 한 번에 설정 가능한 생성자를 만들어두면 편리하다.

 

User.java 일부

public User(String id, String name, String password) {
    this.id = id;
    this.name = name;
    this.password = password;
}

public User() {
}

getCount() 테스트를 만들어 테스트해보자.

 

UserDaoTest.java 일부

@Test
public void count() throws SQLException, ClassNotFoundException{
    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));
}

주의해야 할 점은 두개의 테스트가 어떤 순서로 실행될지는 알 수 없다. JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다.

 

addAndGet() 테스트 보완

id를 조건으로 해서 사용자를 검색하는 기능을 가진 get()에 대한 테스트는 조금 부족하여 검증해보자.

 

UserDaoTest.java일부

@Test
public void addAndGet() throws SQLException, ClassNotFoundException{
    //XML설정파일을 사용한 애플리케이션컨텍스트 초기화
    ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
				
    UserDao dao = context.getBean("userDao",UserDao.class);
		
    User user1 = new User("hehe1","park","1234");
    User user2 = new User("hehe2","park","1234");
		
    dao.deleteAll();
    assertThat(dao.getCount(),is(0));
		
    dao.add(user1);
    dao.add(user2);
		
    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() 예외조건에 대한 테스트

get() 메소드에 전달된 id 값에 해당하는 사용자 정보가 없다면 어떻게 될까?

두 가지 방법이 있을 것이다. 하나는 null과 같은 특별한 값은 리턴하는 것이고, 다른 하나는 id에 해당하는 정보를 찾을 수 없다고 예외를 던지는 것이다.

주어진 id에 해당하는 정보가 없다는 의미를 가진 예외 클래스가 하나 필요한데, 일단 스프링의 EmptyResultDataAccessException 예외를 이용하겠다.

//get()으로 가져온게 없어서 예외를 던져야 성공
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException, ClassNotFoundException {
    ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
		
    UserDao dao = context.getBean("userDao", UserDao.class);
    dao.deleteAll();
    assertThat(dao.getCount(),is(0));
		
    dao.get("unknown_id");
}

이 테스트에서 중요한 것은 @Test 애노테이션의 expected 엘리먼트다. expected는 테스트 메소드 실행 중에 발생하리라 기대하는 예외 클래스를 넣어주면 된다.

@Test에 expected를 추가해놓으면 보통의 테스트와는 반대로, 정상적으로 테스트 메소드를 마치면 테스트가 실패하고, expected에서 지정한 예외가 던져지면 테스트가 성공한다. 예외가 반드시 발생해야 하는 경우를 테스트하고 싶을 때 유용하게 쓸 수 있다.

 

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

주어진 id에 해당하는 데이터가 없으면 EmptyResultDataAccessException을 던지는 get() 메소드를 만들어냈다.

 

UserDao.java일부

public User get(String id) throws ClassNotFoundException, SQLException{
    Connection c = dataSource.getConnection();	//DB 커넥션 관심을 아예 클래스로 분리하여 사용
		
    PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
    ps.setString(1, id);
		
    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;
}

 

포괄적인 테스트

개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있다. 바로 성공테스트만 골라서 만드는 것이다.

이런 이유 때문에 QA팀이나 고객의 인수담당자에 의해 꼼꼼하게 준비된 시나리오르 ㄹ따라 다양한 경우에 대한 전문적인 테스트가 수행될 필요가 있다.

그래서 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다.

 

테스트가 이끄는 개발

get() 메소드의 예외 테스트를 만드는 과정을 다시 돌아보면 한 가지 흥미로운 점을 발견할 수 있다.

 

기능설계를 위한 테스트

가장 먼저 '존재하지 않는 id로 get() 메소드를 실행하면 특정한 예외가 던져져야 한다'는 식으로 만들어야 할 기능을 결정했다.그러고나서 UserDao 코드를 수정하는 대신 getUserFailure() 테스트를 먼저 만들었다.

getUserFailure() 테스트에는 만들고 싶은 기능에 대한 조건과 행위, 결과에 대한 내용이 잘 표현되어 있다.

  단계 내용 코드
조건 어떤 조건을 가지고 가져올 사용자 정보가 존재하지 않는 경우에 dao.deleteAll();
assertThat(dao.getCount(), is(0));
행위 무엇을 할 때 존재하지 않는 id로 get()을 실행하면 get("unknown_id");
결과 어떤 결과가 나온다. 특별한 예외가 던져진다. @Test(expected=EmptyResultDataAccessException.class)

만약 테스트가 실패하면 이떄는 설계한 대로 코드가 만들어지지 않았음을 바로 알 수 있다. 그리고 문제가 되는 부분이 무엇인지에 대한 정보도 테스트 결과를 통해 얻을 수 있다.

결국 테스트가 성공한다면, 그 순간 코드 구현과 테스트라는 두 가지 작업이 동시에 끝나는 것이다.

 

테스트 주도 개발

 만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법을 테스트 주도 개발(Test Driven Development)라 한다. 또는 테스트 우선 개발(Test First Development)라고도 한다.

"실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다."는 것이 TDD의 기본 원칙이다.

TDD의 장점 중 하나는 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧다는 점이다. 개발한 코드의 오류는 빨리 발견할수록 좋다. 빨리 발견된 오류는 쉽게 대응이 가능하기 떄문이다.

 

테스트 코드 개선

UserDaoTest 코드를 잘 보면 반복되는 부분이 있다.

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
User dao = context.getBean("userDao", UserDao.class);

중복된 코드는 메소드 추출 리팩토링 방법도 있지만 이번에는 JUnit이 제공하는 기능을 활용해보자.

 

@Before

로컬 변수인 dao를 테스트 메소드에서 접근할 수 있도록 인스턴스 변수로 변경한다. 그리고 setUp() 메소드에 @Before라는 애노테이션을 추가해준다.

 

UserDaoTest.java

public class UserDaoTest {
    private UserDao dao;
    
    @Before
    public void setUp() {
        private ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.dao = this.context.getBean("userDao",UserDao.class);
    }
	
    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException{
        //생략
    }
	
    @Test
    public void count() throws SQLException, ClassNotFoundException{
        //생략
    }
	
    @Test(expected=EmptyResultDataAccessException.class)
    public void getUserFailure() throws SQLException, ClassNotFoundException {
        //생략
    }
}

 

JUnit의 테스트 수행 방식은 다음과 같다.

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

또 한 가지 꼭 기억해야 할 사항은 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다는 점이다.

JUnit 개발자는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다.

 

픽스처

테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처(fixture)라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이요해 생성해두면 편리하다.

 

UserDaoTest.java 일부

public class UserDaoTest{
    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;

    @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 메소드가 테스트 메소드 개수만큼 박복되기 때문에 애플리케이션 컨텍스트도 세 번 만들어진다.

테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다. 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 해야한다. 이때도 테스트는 일관성 있는 실행 결과를 보장해야 하고, 테스트의 실행 순서가 결과에 영향을 미치지 않아야 한다.

 

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

테스트 컨텍스트의 지원을 받으면 간단한 애노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.

 

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

UserDaoTest.java 일부

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(Locations="/applicatioinContext.xml")
public class UserDaoTest{
    @Autowired
    private ApplicationContext context;
    //생략
    
    @Before
    public void setUp(){
        this.dao = this.context.getBean("userDao", UserDao.class);
        //생략
    }
}

@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션이다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.

@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정한 것이다.

 

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

 UserDaoTest.java의 setUp() 메소드에 System.out.print(this.context)와 System.out.print(this)로 콘솔 로그를 확인해보면 좀 더 확실하게 알 수 있다.
 this.context는 ApplicationContext타입의 context로 테스트 내내 공유한다고 했으므로 같은 주소값이 테스트만큼 나올 것이고 this는 오브젝트 자신이므로 매번 테스트가 끝날 때마다 변하기 때문에 주소값이 다를 것이라고 예상할 수 있다. 실제로 돌려보면 주소값이 this.context는 3번 다 똑같고 this는 3번 다 다름을 확인할 수 있다.

 

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

스프링 테스트 컨텍스트 프레임워크의 기능은 하나의 테스트 클래스 안에서 애플리케이션 컨텍스트를 공유해주는 것 외에도 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.

 

@Autowired

Autowired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다.

스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다. 따라서 애플리케이션 컨텍스트에는 ApplicationContext 타입의 빈이 존재하는 셈이고 DI도 가능한 것이다.

@Autowired를 이용해 애플리케이션 컨텍스트가 갖고 있는 빈을 DI 받을 수 있다면 굳이 컨텍스트를 가져와 getBEan()을 사용하는 것이 아니라, 아예 UserDao 빈을 직접 DI 받을 수도 있을 것이다.

 

UserDaoTest.java 일부

public class UserDaoTest{
    @Autowired
    UserDao dao;
    
    //생략
}

applicationContext.xml 일부

<bean id="userDao" class="springbook.user.dao.UserDao">
    <property name="dataSource" ref="dataSource"/>
</bean>

단, @Autowired는 같은 타입의 빈이 두개 이상있는 경우 변수의 이름과 같은 이름의 빈이 있는지 확인한다.

 

DI와 테스트

DI를 적용해야 하는 이유는 다음과 같다.

  • 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다.
  • 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 달ㄴ 차원의 서비스 기능을 도입할 수 있다.
  • 효율적인 테스트를 손쉽게 만든다.
    • 테스트를 잘 활용하려면 가능한 한 작은 단위의 대상에 국한해서 테스트해야한다.

테스트에 DI를 이요하는 방법 몇 가지 살펴보자

 

테스트 코드에 의한 DI

 운영용 applicationContext.xml을 테스트할 때 사용하려면 어떻게 해야할까?xml 내부의 dataSource를 직접 고치는 방법도 있지만 테스트 클래스 내부에서 테스트 전용 코드를 만들어 DI해주는 방법도 있다. 다음과 같다.

 

UserDaoTest.java 일부

public class UserDaoTest {
 
    @Autowired
    UserDao dao;
	
    @Before
    public void setUp() {
        DataSource dataSource = new SingleConnectionDataSource(
            "jdbc:mysql//localhost/testdb", "spring","book",true);
        dao.setDataSource(dataSource);
        
        //생략
    }
    //생략
}

위의 테스트 코드는 애플리케이션 컨텍스트에서 가져온 UserDao 빈의 의존관계를 강제로 변경한다. 한 번 변경하면 나머지 모든 테스트를 수행하는 동안 변경된 애플리케이션 컨텍스트가 계속 사용될 것이다.

그래서 @DirtiesContext라는 애노테이션을 추가해줬다. 이 애노테이션은 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다.

 

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

테스트 코드에서 빈 오브젝트에 수동으로 DI 하는 방법은 단점이 많다.

테스트에서 사용될 DataSource 클래스가 빈으로 정의되 테스트 전용 설정파일을 따로 만들어두는 방법을 이용해도 된다.

 

컨테이너 없는 테스트

마지막으로 살펴볼, DI를 테스트에 이용하는 방법은 아예 스프링 컨테이너를 사용하지 않고 테스트를 만드는 것이다.

스프링 테스트임에도 스프링 컨테이너가 필수가 아닌 경우가 많다. 스프링 컨테이너를 띄우지 않아도 되는 테스트라면 띄우지 않는 것이 가장 좋다.

 

침투적 기술과 비침투적 기술

  • 침투적 기술 : 애플리케이션 코드가 특정 기술 관련 API, 인터페이스, 클래스에 종속되는 것
  • 비침투적 기술 : 기술에 종속되지 않는 순수한 코드를 유지할 수 있게 해주는 것.

 

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

그렇다면 DI 테스트에 이용하는 세 가지 방법 중 어떤 것을 선택해야할까?

  •  항상 스프링 컨테이너 없이 테스트할 수 있는 바법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.
  •  여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우가 있다. 이때는 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다.
  • 예외적인 의존관계를 강제로 구성해야 할때는 수동 DI 해서 테스트하는 방법을 사용하면 된다.

 

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

때로는 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서도 테스트를 작성해야 한다. 이런 테스트를 학습 테스트(Learning test)라 한다.

 학습 테스트의 목적은 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히려는 것이다.

 

학습 테스트의 장점

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
    • 자동화된 테스트 코드로 만들어지기 때문에 다양한 조건에 따라 기능이 어떻게 동작하는지 빠르게 확인할 수 있다.
  • 학습 테스트 코드르 개발 중에 참고할 수 있다.
    • 학습 테스트는 다양한  기능과 조건에 대한 테스트 코드를 개별적으로 만들고 남겨둘 수 있다.
  • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
    • 기존에 사용했던 API나 기능에 변화가 있거나 업데이트된 제품에 버그가 있다면 미리 확인할 수 있다.
  • 테스트 작성에 대한 좋은 훈련이 된다.
    • 학습 테스트를 작성해보면서 테스트 코드 작성을 연습할 수 있다.
    • 학습 테스트는 한두 가지 간단한 기능에만 초점을 맞추면 되기 떄문에 테스트도 대체로 단순하다.
    • 새로운 테스트 방법을 연구하는 데도 도움이 된다.
  • 새로운 기술을 공부하는 과정이 즐거워진다.

 

버그 테스트

버그 테스트란 코드에 오류가 있을 때 그 요류를 가장 잘 드러내줄 수 있는 테스트를 말한다. 버그 테스트는 일단 실패하도록 만들어야 한다. 버그가 원인이 되서 테스트가 실패하는 코드를 만드는 것이다.

버그 테스트의 필요성과 장점은 다음과 같다.

  • 테스트의 완성도를 높여준다.
    • 기존 테스트에서는 미처 검증하지 못했던 부분이 있기 때문에 오류가 발생한 것이기에 테스트로 인해 보완시킬 수 있다.
  • 버그의 내용을 명확하게 분석하게 해준다.
    • 버그가 있을 때 그것을 테스트로 만들어서 실패하게 하려면 어떤 이유 때문에 문제가 생겼는지 명확히 알아야 한다. 따라서 버그를 좀 더 효과적으로 분석할 수 있다.
  • 기술적인 문제를 해결하는 데 도움이 된다.

 

[참고]

토비의스프링

https://roadofdevelopment.tistory.com/48?category=463221 

https://jake-seo-dev.tistory.com/19?category=906606 

 

'Spring' 카테고리의 다른 글

토비의 Spring_Chapter04_예외  (0) 2022.10.27
토비의 Spring_Chapter03_템플릿  (0) 2022.10.19
토비의 Spring_Chapter01_오브젝트와 의존관계  (0) 2022.10.11
SpringBoot_Chapter01_개요  (0) 2022.10.05
Spring_Chapter08_Transaction  (0) 2022.05.12