• [토비의 스프링 vol.1]2장 테스트

    2021. 5. 12.

    by. 웰시코더

    반응형

    -토비의 스프링 3.1 vol.1의 2장을 정리 및 실습한다.


    2.1 UserDaoTest 다시 보기

    2.1.2 UserDaoTest의 특징

     1장에서 테스트한 UserDaoTest의 특징은 다음과 같다.

    • 가장 손쉬운 테스트 방법인 main()을 사용
    • UserDao의 오브젝트를 가져와서 메소드 호출
    • 테스트 전용 입력값(User 오브젝트)를 직접 만들어 인자로 넣음
    • 콘솔에 결과 출력

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

     일반적으로 DAO를 테스트하는 방법은 서비스 계층, MVC 프레젠테이션 계층까지 포함한 모든 입출력 기능을 대충이라도 만든 후 서버에 배치한 후에 웹 화면에서 직접 값을 입력하여 버튼을 눌러 등록하여 테스트한다. 이러한 방법의 문제점은 다음과 같다.

    • 시간이 오래 걸림
    • 에러 발생시 정확히 어떤 부분인지 캐치하는 것이 느려짐

    작은 단위 테스트

     테스트 대상이 명확하다면 그 대상만 집중 테스트를 하는 것이 좋다. 관심사의 분리 원리가 여기에도 적용된다. 테스트 관심 대상에 맞게 대상을 분리하여 접근해야 한다. 

     UserDaoTest는 UserDao라는 대상과 관심사에게 집중할 수 있게 작은 단위로 만들어진 테스트다. 이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트(unit test)라고 한다. 단위의 크기는 상대적인 것으로 add()만 단위가 될수도 있고 사용자 관리 기능 전체를 하나의 단위로 볼 수도 있다. 단위는 작을수록 좋다.

     

    자동수행 테스트 코드

     웹화면 띄우기, 서버에 배치 등의 과정 없이 main() 메소드만 실행하여 테스트 전 과정을 수행한 UserDaoTest가 자동수행 테스트 코드이다. 

    2.1.3 UserDaoTest의 문제점

    • 수동 확인 작업이 번거롭다. 자동화 테스트를 main()을 통해 만들었지만, 모두 사람의 눈으로 결과를 판별해야 한다. System.out.print()를 통하여 사람이 테스트가 잘 되었는지 판별해야하기 때문에 사람의 책임이 크다. 그렇기 때문에 결과에 대한 판별을 실수할 수 있다.
    • 실행 작업이 번거롭다. 간단히 실행 가능한 main()이라도 DAO가 수백개고 그에 대한 main()도 수백개면 수백번 실행을 해야한다.

     

    2.2 UserDaoTest 개선

    2.2.1 테스트 검증의 자동화

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

     

    UserDaoTest.java

    public class UserDaoTest {
        public static void main(String[] args) throws ClassNotFoundException, SQLException {
            //자바설정파일을 사용한 애플리케이션컨텍스트 초기화
            //ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);	//DaoFactory 설정정보를 생성자의 인자로 줌
            //UserDao dao = context.getBean("userDao", UserDao.class);	//getBean(메소드명, 리턴타입)
    		
            //XML설정파일을 사용한 애플리케이션컨텍스트 초기화
            ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
    		
            UserDao dao = context.getBean("userDao",UserDao.class);
    		
            //테스트용 데이터 셋팅
            User user = new User();
            user.setId("xmlMan3");
            user.setName("kimMinSu");
            user.setPassword("1234");
    		
            //dao 함수 호출
            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() + " 조회 성공");
            
            //수정후
            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 테스트의 효율적인 수행과 결과 관리

     위의 main() 테스트를 좀 더 편리하게 수행하기 위해 JUnit이라는 테스트 도구를 적용한다. 자바 테스팅 프레임워크인 JUnit은 자바로 단위 테스트시 유용하다.

     

    테스트 메소드 전환

     JUnit 프레임워크의 요구 조건은 두 가지가 있다.

    1. 메소드가 public으로 선언돼야 함
    2. 메소드에 @Test 어노테이션 붙이기

    아래는 JUnit 프레임워크를 적용한 테스트 코드이다.

     

    UserDaoTest.java

    public class UserDaoTest {
        @Test
        public void addAndGet() throws SQLException, ClassNotFoundException{
            //XML설정파일을 사용한 애플리케이션컨텍스트 초기화
            ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
    				
            UserDao dao = context.getBean("userDao",UserDao.class);
    		
            //테스트용 데이터 셋팅
            User user = new User();
            user.setId("xmlMan6");
            user.setName("kimMinSu");
            user.setPassword("1234");
    		
            //dao 함수 호출
            dao.add(user);
    		
            System.out.println(user.getId() + " 등록 성공");
    		
            User user2 = dao.get(user.getId());
            
            //if조건비교와 같은 기능의 assertThat
            assertThat(user2.getName(), is(user.getName()));
            assertThat(user2.getPassword(), is(user.getPassword()));
        }
    	
        //@Test어노테이션 메소드를 실행하기 위해 최초 main 작성
        public static void main(String[] args) {
            JUnitCore.main("springbook.user.dao.UserDaoTest");
        }
    }

     assertThat()은 JUnit이 제공하는 메소드로 앞,뒤의 인자를 매쳐(is()) 조건으로 비교하는 기능을 제공한다. 성공하면 다음으로 넘어가지만 실패하면 java.lang.AssertError를 발생시킨다.

     

     

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

    2.3.1 JUnit 테스트 실행 방법

     JUnitCore를 통해 콘솔로 출력결과를 보는 방법도 있지만 자바 IDE(이클립스)에 내장된 JUnit 테스트 지원 도구 사용이 더 유용하다. 이를 사용하면 JUnitCore을 사용할 때처럼 main()을 만들 필요가 없다. 이클립스 Run As-JUnit Test를 선택하면 @Test어노테이션이 적용된 메소드를 테스트한다. 테스트에 성공하면 이클립스에 전용 확인 View가 나타나고 초록색 bar가 뜨면 성공, 붉은 bar가 뜨면 실패이다.

    테스트 성공

     

    테스트 실패(PK 에러 발생)

     

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

     

    2.3.2 테스트 결과의 일관성

     현재 테스트의 문제는 실행할 때마다 DB에서 테이블 데이터를 삭제해줘야 PK 에러가 발생하지 않는다는 것이다. 그래서 addAndGet() 테스트가 끝나면 사용자 정보를 삭제해서 테스트 수행 이전으로 만들어주려고 한다. 

     

    deleteAll(), getCount() 추가

     UserDao 클래스에 deleteAll() 기능을 추가한다. 모든 User 테이블의 레코드를 삭제하는 기능이다.

     

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

     

     위의 만든 두 가지 기능을 addAndGet() 테스트에 추가하여 테스트한다. 최초에 deleteAll()을 실행한 후 getCount()를 실행하여 0이 맞는지 먼저 확인한다. 그리고 add() 실행 후 getCount()를 다시 실행하여 1이 맞는지 확인하면 된다.

     

    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("xmlMan7");
        user.setName("kimMinSu");
        user.setPassword("1234");
    		
        //dao 함수 호출
        dao.add(user);
        assertThat(dao.getCount(),is(1));	//add() 이후 데이터 개수가 1인지 확인
    		
        System.out.println(user.getId() + " 등록 성공");
    		
        User user2 = dao.get(user.getId());
        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));	
    }

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

     

    getCount() 테스트

     좀 더 꼼꼼하게 getCount()를 테스트해본다. add()를 수행할 때마다 getCount() 값이 1씩 증가하는지 정확히 확인해보는 테스트를 만든다. 여러개의 User 데이터를 만들기 위해 User.java에 생성자를 추가한다.

     

    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("hihi1","kim","1234");
        User user2 = new User("hihi2","kim","1234");
        User user3 = new User("hihi3","kim","1234");
    		
        dao.deleteAll();
        assertThat(dao.getCount(), is(0));
    		
        //add할때마다 1,2,3씩 증가하는 것을 확인
        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() 테스트 보완

     add()의 기능 검증은 어느정도 되었지만 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()));
    }

     user1,user2 테스트 데이터를 만든 후 add()를 한다. 그리고 user1,user2의 아이디를 인자로 하여 get()으로 데이터를 가져온다. 가져온 데이터(userget1,userget2)와 user1,user2의 데이터를 비교하여 get() 테스트를 완료하였다.

     

    get() 예외조건에 대한 테스트

     get()으로 주어진 id에 맞는 유저데이터가 없는 경우를 처리한다. 스프링이 정의한 데이터 엑세스 예외 클래스 EmptyResultDataAccessException을 이용한다. 이 클래스가 import가 안 되어서 확인해보니 spring-dao-2.0.3.jar 라이브러리를 추가하지 않아서였다. 추가한 후 다음과 같은 테스트 메소드를 만든다.

     

    UserDaoTest.java 일부

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

     expected에 EmptyResultDataAccessException.class를 지정해줬다. 이렇게 하면 이 예외가 던져질 때 성공(녹색bar)인 테스트가 만들어진다. 테스트 내용은 모든 user데이터를 지운 후(deleteAll()) get()으로 없는 아이디(unknown_id) 정보를 가져오려고 할 때 예외를 던지는 테스트이다.

     그러나 실행해보면 테스트는 실패(붉은bar)한다. UserDao.java의 get()을 실행할 때 rs.next()로 가져올 값이 없기 때문에 SQLException을 던지게 된다. 그래서 다음과 같은 처리가 필요하다.

     

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

     rs.next() 실행 시 rs에 데이터가 있는 경우 user 객체에 담아준다. rs,ps,c를 close()한 후, user 변수가 null이면 예외를 던져주게 수정했다. 이후 위에서 만든 테스트 메소드인 getUserFailure()를 다시 junit으로 돌리면 정상적으로 녹색 bar로 성공하는 것을 확인할 수 있다. 즉, 기대한 예외가 던져진 것이다.

     

    테스트 주도 개발(TDD)

     위의 get() 메소드 수정은 수정부터 하고 테스트한 것이 아니라, getUserFailure()라는 테스트 코드를 먼저 작성한 후에 필요에 맞게 수정한 것이다. 이처럼 테스트 코드부터 만들고 테스트를 성공하게 해주는 코드를 작성하는 방식을 테스트 주도 개발(TDD)라고 한다. 

     

    2.3.5 테스트 코드 개선

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

    • 1.테스트 클래스에서 @Test가 붙은 public void 테스트 메소드 모두 찾기
    • 2.테스트 클래스의 오브젝트 하나 만들기
    • 3.@Before 붙은 메소드 실행
    • 4.@Test 붙은 메소드 호출 후 결과 저장
    • 5.@After 붙은 메소드 실행
    • 6.나머지 테스트 메소드에 대해 2~5 반복
    • 모든 테스트 결과를 종합해서 결과 도출

    @Before

     UserDaoTest.java의 테스트 코드들에는 중복되는 코드가 있다. ApplicationContext 변수에 설정정보를 초기화하고 getBean()으로 빈객체를 가져오는 부분이다. 메소드 추출 등의 방법이 아니라 junit 제공 어노테이션 @Before를 사용하면 JUnit은 @Test 메소드를 실행하기 전에 @Before가 붙은 메소드를 자동으로 실행한다. @After라는 메소드도 있는데 말 그대로 @Test 메소드 실행이 다 끝나면 후에 자동으로 실행한다.

     중요한 것은 테스트 클래스 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진 후 다시 클래스의 오브젝트를 만든다. 이는 각 테스트가 서로 영향을 안 주도록 독립적인 실행을 확실히 보장해주기 위함이다.

     

    픽스처

     테스트 시에 필요한 정보, 오브젝트 등을 픽스처라고 한다. UserDaoTest의 dao, User 오브젝트들 모두 픽스처이다. 중복제거를 위해 아래와 같이 setUp() 메소드를 만들어 @Before를 적용한다.(ApplicationContext 초기화 관련 코드도 중복되니 setUp() 메소드에 뺀다)

     

    UserDaoTest.java 일부

    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("fixture1","kim","1234");	
        this.user2 = new User("fixture2","jin","1234");
        this.user3 = new User("fixture3","yoon","1234");
    }
    

     

     

    2.4 스프링 테스트 적용

     @Before 메소드가 테스트 메소드 개수만큼 반복되어 애플리케이션 컨텍스트도 반복해서 만들어져 생성 시간이 많이 걸리는 문제가 있다. 테스트는 매번 새로운 오브젝트를 만들어 독립성을 유지해주는게 좋지만 애플리케이션 컨텍스트 같은 경우는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.

     @BeforeClass 스태틱 메소드를 사용하면 매번 테스트 클래스의 오브젝트를 만들 때 생성하는 게 아니라 스태틱 변수에 저장하여 공유 사용할 수 있다.

     

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

     @BeforeClass 애노테이션이 아니라 스프링이 제공하는 지원 기능을 사용하는 것이 더 편리하고 좋다. 다음과 같이 UserDaoTest.java를 수정한다.

     

    UserDaoTest.java

    @RunWith(SpringJUnit4ClassRunner.class)	//어노테이션 추가
    @ContextConfiguration(locations = "/applicationContext.xml")	//어노테이션 추가
    public class UserDaoTest {
        @Autowired
        private ApplicationContext context;	//실행시 인스턴스가 이 변수에 값 자동 주입
    	
        private UserDao dao;
        private User user1;
        private User user2;
        private User user3;
    	
        @Before
        public void setUp() {
            this.dao = this.context.getBean("userDao",UserDao.class);
            this.user1 = new User("fixture1","kim","1234");
            this.user2 = new User("fixture2","jin","1234"); 
            this.user3 = new User("fixture3","yoon","1234");
        }
    	
        @Test
        public void addAndGet() throws SQLException, ClassNotFoundException{
            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()));
        }
    	
        @Test
        public void count() throws SQLException, ClassNotFoundException{
            User user1 = new User("hihi1","kim","1234");
            User user2 = new User("hihi2","kim","1234");
            User user3 = new User("hihi3","kim","1234");
    		
            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));
        }
    	
        //get()으로 가져온게 없어서 예외를 던져야 성공
        @Test(expected=EmptyResultDataAccessException.class)
        public void getUserFailure() throws SQLException, ClassNotFoundException {
            dao.deleteAll();
            assertThat(dao.getCount(),is(0));	 
            dao.get("unknown_id");
        }
    	
        public static void main(String[] args) {
            JUnitCore.main("springbook.user.dao.UserDaoTest");
        }
    }

     기존의 ApplicationContext 초기화 코드는 삭제하고난 후, @RunWith, @ContextConfiguration 어노테이션을 추가한다. @ContextConfiguration 어노테이션의 locations에는 applicationContext.xml의 설정파일을 입력한다. 그리고 @Autowired를 사용하여 ApplicationContext 타입 변수를 선언해준다. 이렇게 하면 모든 테스트 메소드가 공유할 수 있는 ApplicationContext를 만들 수 있다.

     참고로 org.springframework.test-3.0.7.RELEASE.jar를 추가해줘야 @RunWith에 SpringJUnit4ClassRunner.class를 지정할 수 있다. SpringJUnit4ClassRunner.class는 스프링의 테스트 컨텍스트 프레임워크의 JUnit 확장 기능이다. 

     그런데 실행하니 초록색 bar가 아닌 실패의 붉은 bar가 떠버렸다..에러를 보니 aopUtils라는 곳에서 클래스를 찾을 수 없다는 것 같았는데 spring-aop-3.0.7.RELEASE.jar도 빌드패스에 추가해주니 해결되었다. 아마 내부적으로 aop를 사용하는게 아닌가 싶다.  

     

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

     UserDaoTest.java의 setUp() 메소드에 System.out.print(this.context)와 System.out.print(this)로 콘솔 로그를 확인해보면 좀 더 확실하게 알 수 있다.

     this.context는 ApplicationContext타입의 context로 테스트 내내 공유한다고 했으므로 같은 주소값이 테스트만큼 나올 것이고 this는 오브젝트 자신이므로 매번 테스트가 끝날 때마다 변하기 때문에 주소값이 다를 것이라고 예상할 수 있다. 실제로 돌려보면 주소값이 this.context는 3번 다 똑같고 this는 3번 다 다름을 확인할 수 있다.

     이러면 메번 context를 생성 안 하기 때문에 점점 테스트 실행속도가 빨라진다.(처음 초기화만 느림)

     

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

     스프링 테스트 컨텍스트 프레임워크의 기능은 하나의 테스트 클래스 안에서 ApplicationContext 공유 뿐만 아니라, 다른 클래스들까지 공유 가능하다. 두 테스트 클래스에서 @ContextConfiguration(locations="/applicationContext.xml")로 설정해주면 두 클래스 모두 같은 설정파일을 공유한다.

     

    @Autowired

     스프링 DI에 사용되는 애노테이션이다. 수정자 메소드나 생성자가 있어야 설정파일에서 빈을 읽어 주입할 수 있지만 이 경우에는 이런 것들이 없어도 설정파일에 같은 타입의 빈이 있다면 @Autowired 애노테이션이 붙은 인스턴스 변수에 주입해준다.

     앞의 ApplicationContext.xml의 경우에는 내부에 빈을 정의해두지는 않았지만, 스프링 애플리케이션 컨텍스트 초기화 시에 자기 자신도 빈으로 등록하기 때문에 ApplicationContext 타입의 빈이 존재하는 셈이 되고 DI가 된 것이다. UserDao도 getbean() 방식이 아니라 @Autowired 방식으로 다음과 같이 바꿀 수 있다.

     

    UserDaoTest.java 일부

    @Autowired
    UserDao dao;

    applicationContext.xml 일부

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

     참고로 같은 타입 빈이 2개 이상일 때, 빈 id와 @Autowired 변수명과 일치하는 것으로 가져온다.

     

    2.4.2 DI와 테스트

    테스트 코드에 의한 DI

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

     

    UserDaoTest.java 일부

    @DirtiesContext
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "/applicationContext.xml")
    public class UserDaoTest {
        @Autowired
        private ApplicationContext context;
    
        @Autowired
        UserDao dao;
    	
        private User user1;
        private User user2;
        private User user3;
    	
        @Before
        public void setUp() {
            DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:mysql//localhost/testdb", "spring","book",true);
            dao.setDataSource(dataSource);
            
            this.user1 = new User("fixture1","kim","1234");
            this.user2 = new User("fixture2","jin","1234");
            this.user3 = new User("fixture3","yoon","1234");
        }
        
        //코드 생략
    }

    @DirtiesContext를 사용하면 이 애노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 하지 않는다.

     

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

     위의 방법보다 test-applicationContext.xml을 만들어 사용하는 것이 더 좋다.

     

    컨테이너 없는 DI 테스트

     스프링 컨테이너 없이 테스트를 만드는 방법도 있다. UserDao의 초기화와 dataSource의 초기화를 setUp()에서 수동으로 DI하여 사용할 수 있다. @RunWith,@Autowired 등의 스프링 애노테이션을 제거하고 다음과 같이 작성할 수 있다.

     

    UserDaoTest.java 일부

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

     이렇게 하면 애플리케이션 컨텍스트가 만들어지지 않기 때문에 더 빠른 테스트가 가능하다.

     

     

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

     프레임워크, 라이브러리, 다른 개발자가 만든 코드 등을 학습 목적으로 테스트하는 테스트 방법을 학습 테스트라고 한다. 기능 검증이 아닌 기능 학습이 목적이다. 새로운 프레임워크 등을 사용하게 될 때 유용하다.

     

    2.5.1 학습 테스트 장점

    • 다양한 조건에 따른 기능 동작 케이스를 쉽게 확인할 수 있음
    • 개발 중에 학습 테스트 코드를 참고 가능
    • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와줌.버전 업데이트된 프레임워크 등은 잠재적 버그가 있을 수 있기 때문에 학습테스트를 이용해 미리 확인할 수 있음
    • 태스트 작성에 대한 훈련이 됨

    2.5.2 학습 테스트 예제 

    JUnit 테스트 오브젝트 테스트

     JUnit은 테스트 메소드를 수행할 때마다 새롭게 오브젝트가 만들어지는데 실제로 그런지 테스트할 수 있다. 첫번째 테스트에서 this(현재 실행되는 테스트의 JUnitTest 오브젝트 주소값)와 static JUnitTest 오브젝트 변수 testObject(전 테스트 때 JUnitTest 오브젝트 주소값)를 비교한다. 다르면 통과기 때문에 testObject에 this 값을 넣어 2번째 테스트 때 비교할 수 있게 한다. 2번째 테스트도 같은 코드를 넣어 비교하여 3번째 테스트와 비교할 수 있게 한다. 다음과 같다.

     

    JUnitTest.java

    package springbook.learningtest.junit;
    
    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.CoreMatchers.not;
    import static org.hamcrest.CoreMatchers.sameInstance;
    import static org.junit.Assert.assertThat;
    
    import org.junit.Test;
    
    public class JUnitTest {
    	static JUnitTest testObject;
    	
    	@Test
    	public void test1() {
    		assertThat(this, is(not(sameInstance(testObject))));
    		testObject = this;
    	}
    	@Test
    	public void test2() {
    		assertThat(this, is(not(sameInstance(testObject))));
    		testObject = this;
    	}
    	@Test
    	public void test3() {
    		assertThat(this, is(not(sameInstance(testObject))));
    		testObject = this;
    	}
    }
    

     sameInstance()는 실제로 같은 오브젝트인지 비교하는 메소드이다. 테스트를 돌려보면 성공이다. 그러나 위의 방법은 첫번째 test1()과 test3()의 테스트 오브젝트 주소값이 같은 경우를 비교할 수 없다. testOjbect에는 전 테스트의 JUnitTest 오브젝트의 주소값만 담겨있기 떄문이다.

     

     다음과 같이 개선한다. static Set타입의 변수를 만든다. 테스트 메소드를 비교할 때 Set 타입 변수 내부에 현재 테스트 메소드(test1)의 테스트 클래스 오브젝트(this)가 있는지 체크 후, 없으면 add한다. 다음 테스트 메소드의 테스트 클래스 오브젝트(this)가 Set 타입 변수에 있는지 확인한다.

     

    JUnitTest.java

    package springbook.learningtest.junit;
    
    import static org.hamcrest.CoreMatchers.not;
    import static org.junit.Assert.assertThat;
    import static org.junit.matchers.JUnitMatchers.hasItem;
    
    import java.util.HashSet;
    import java.util.Set;
    
    import org.junit.Test;
    
    public class JUnitTest {
        static Set<JUnitTest> testObjects = new HashSet<JUnitTest>();
    	
        @Test
        public void test1() {
            assertThat(testObjects, not(hasItem(this)));	//hasItem은 컬렉션의 원소여부를 검사
            testObjects.add(this);
        }
        @Test
        public void test2() {
            assertThat(testObjects, not(hasItem(this)));	//hasItem은 컬렉션의 원소여부를 검사
            testObjects.add(this);
        }
        @Test
        public void test3() {
            assertThat(testObjects, not(hasItem(this)));	//hasItem은 컬렉션의 원소여부를 검사
            testObjects.add(this);
        }
    }
    

     

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

     스프링의 테스트용 애플리케이션 컨텍스트는 테스트 개수와 상관없이 1개만 만들어지는 것이 맞는지 테스트한다.

    새로운 설정파일을 다음과 같이 만든다.

     

    junit.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans" 
    		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    		xsi:schemaLocation="http://www.springframework.org/schema/beans 
    						http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    </beans>

     애플리케이션 컨텍스트가 하나만 생성되는지 확인하는 것이 목적이기 때문에 xml 내부에는 따로 특정한 코드를 작성할 필요 없다.

     xml 설정파일을 만든 후, JunitTest에서 애플리케이션 컨텍스트 공유 여부의 테스트를 진행하도록 코드를 작성한다. 다음과 같다.

     

    JunitTest.java

    package springbook.learningtest.junit;
    
    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.CoreMatchers.not;
    import static org.hamcrest.CoreMatchers.nullValue;
    import static org.junit.Assert.assertThat;
    import static org.junit.Assert.assertTrue;
    import static org.junit.matchers.JUnitMatchers.hasItem;
    import static org.junit.matchers.JUnitMatchers.either;
    
    
    import java.util.HashSet;
    import java.util.Set;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "/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);
    		
            //assertThat보다 간결
            assertTrue(contextObject == null || contextObject == this.context);
            contextObject = this.context;
        }
        @Test
        public void test3() {
            assertThat(testObjects, not(hasItem(this)));
            testObjects.add(this);
    		
            //either는 2개의 메처 중 하나만 true면 성공
            assertThat(contextObject, 
                     either(is(nullValue())).or(is(this.context)));
            contextObject = this.context;
        }
    }

     assertTrue(), either(), nullValue() 등 새로운 매처 메소드들을 사용하였다. 스프링 테스트 컨텍스트의 테스트이기 때문에 @RunWith와 @ContextConfiguration을 다시 붙여준다. 그리고 ApplicationContext 타입 변수 context를 @Autowired로 설정하여 선언한다. 이러면 테스트 구동 시에 설정파일이 자동으로 DI되어 담기게 된다. 그리고 static 변수로 ApplicationContext 타입의 contextObject라는 변수를 null로 선언한다. 이제 test 메소드를 실행할 때마다 context 변수의 값이 contextObject와 같은지 체크할 수 있다. 최초에는 contextObject가 null이기 때문에 tests1(), test2(), test3() 모두 contextObject의 null 체크를 한다.

     

    2.5.3 버그 테스트

     코드 오류가 있을 때 오류를 드러내줄 수 있는 테스트이다. 오류 발견 피드백을 받았다면 바로 코드 수정을 하기 전에 버그 테스트를 진행하면 좋다.

     

    참고자료 : 토비의 스프링 3.1 Vol.1

    반응형

    댓글