'단위 테스트'에 해당되는 글 1

  1. 2010.06.02 단위 테스트에서 외부 자원을 대체하기

단위 테스트에서 외부 자원을 대체하기

테스트를 할때 데이터베이스 연결, 네트워크상에 존재하는 자원 참조등 외부 자원을 사용해야 하는 경우에는 단위 테스트를 하기가 쉽지 않다.
단위 테스트는 테스트 대상이 제대로 동작하는지 검사하는 것이 목적이므로 여기에 집중하기 위해 외부 자원, 의존 클래스를 Mock 객체로 대체한다.

- 참고
Mock Object 를 사용해서 쉽게 테스트 하기
Using mock objects for complex unit tests

- 우리가 일반적으로 작성하는 DAO
public class DatabaseExample {
    public String readAbc(Connection con, String tableName) throws SQLException{
        int resultNum = 1;
        Statement st = con.createStatement(); //대상(#1)
        ResultSet rs = st.executeQuery("SELECT empno, empid FROM " + tableName); //대상(#1)
        String result = "";
        
        while(rs.next()){ //대상(#2)
            int no = rs.getInt(1); //대상(#2)
            String id = rs.getString(2); //대상(#2)
            result += resultNum++ + ":" + no + "," + id + "\n";
        }
        return result;
    }
}

- 데이터베이스없이 테스트가 가능하도록 Mock객체를 만든다.(대상 #1)
public class MockConnection implements Connection {
    ...
    @Override
    public Statement createStatement() throws SQLException {
        return new MockStatement();
    }
}

public class MockStatement implements Statement {
    ...
    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
        return new MockResultSet();
    }
}

public class MockResultSet implements ResultSet

- 테스트 코드를 작성한다.
public class DatabaseExampleTest {
    @Test
    public void testReadAbc() throws SQLException {
        Connection con = new MockConnection();
        DatabaseExample example = new DatabaseExample();
        assertEquals("1:101,John\n", example.readAbc(con, "TestTable"));
    }
}

- 결과는 테스트 실패다.
org.junit.ComparisonFailure: expected:<[1:101,John]> but was:<[]>
MockResultSet.next()가 제대로 구현되지 않아서 false를 리턴해서 결과적으로 테스트가 실패했다.

- 테스트를 다시 작성한다.
public class DatabaseExampleTest {
    @Test
    public void testReadAbc() throws SQLException {
        
        MockResultSet rs = new MockResultSet();
        rs.addRow(new Object[]{101, "John"});
        
        MockStatement st = new MockStatement();
        st.setResultSet(rs);
        
        MockConnection con = new MockConnection();
        con.setStatement(st);
        
        DatabaseExample example = new DatabaseExample();
        assertEquals("1:101,John\n", example.readAbc(con, "TestTable"));
    }
}

- Mock 객체를 다시 작성한다.(대상 #2)
외부에서 입력한 값에 따라 결과를 반환하도록 구현한다.
public class MockResultSet implements ResultSet {

	List<Object[]> list;
	Object[] currentRow;
	int index = -1;
	
	public MockResultSet(){
		list = new ArrayList<Object[] >();
	}
	
	public void addRow(Object[] row){
		list.add(row);
	}

	@Override
	public boolean next() throws SQLException {
		if(++index < list.size()){
			currentRow = list.get(index);
			return true;
		}else{
			return false;
		}
	}
	
	@Override
	public String getString(int columnIndex) throws SQLException {
		return currentRow[columnIndex - 1].toString();
	}
	
	@Override
	public int getInt(int columnIndex) throws SQLException{
		return ((Number)currentRow[columnIndex - 1]).intValue();
	}
	...
}

- 필드로 뺄수 있는 부분은 빼고 setUp()를 활용한다.
결과가 여러 줄을 리턴하는 경우 테스트도 추가한다.