DbUnit 개념
근래에 만들어지는 프로그램들 중에서 데이터베이스를 사용하지 않는 것은 드물다고 할 수 있다.
그런데 이 데이터베이스 부분을 포함한 테스트(DAO)를 할 때에는 BO나 Controller과는 다르게 데이터베이스를 직접 Access하기 때문에 테스트하기 힘든 부분이 있다.
테스트 코드가 데이터베이스의 상태에 의존적으로 작성되게 된다면 깨지기 쉬운 테스트 코드가 된다는 점이다.
예를 들어서 다음과 같은 테스트 코드를 작성했다고 하자.
@Test
public void testSelectPersonById() {
Person person = personDAO.selectPersonById("happy");
assertThat(person.getId(), is("happy"));
assertThat(person.getName(), is("john"));
}
PersonDAO의 selectPersonById라는 메소드에 대한 테스트로, Id값으로 Person 정보를 조회하는 메소드에 대한 테스트 케이스이다.
이 테스트 케이스는 데이터베이스에 실제로 아이디는 happy이고 이름은 john인 레코드가 있어야만 성공할 수 있다.
데이터베이스 테이블에 위의 내용에 대한 레코드가 없으면 실패하게되는 문제점이 있는 것이다.
이러한 문제점을 보완하기 위해 다음과 같은 코드를 생각해볼 수 있다.
@Test
public void testSelectPersonById() {
personDAO.deleteAll();
Person person = new Person();
person.setId("happy");
person.setName("john");
personDAO.insertPerson(person);
Person person = personDAO.selectPersonById("happy");
assertThat(person.getId(), is("happy"));
assertThat(person.getName(), is("john"));
}
이 코드에서는 데이터베이스 상태에 의존적이지 않도록 조회를 하기 전에 조회할 데이터를 미리 삽입하고 있다. 또 중복된 데이터 삽입 시도로 인한 오류를 막기위해서 삽입 전에 모든 행을 삭제하는 동작도 추가하였다.
하지만 여기서도 문제점을 한가지 발견할 수 있다. 이 테스트 케이스는 데이터베이스 상태에 의존적이 되지 않을지는 몰라도, 테스트의 성공 여부가 테스트 하고자하는 selectPersonById
메소드의 정상 동작 여부 뿐만 아니라, deleteAll
, insertPerson
메소드의 정상 동작 여부에도 의존하게 된다.
selectPersonById
메소드는 정상적으로 동작할지라도 insertPerson
메소드나 deleteAll
메소드에 문제점이 있으면 실패할 수 있는 테스트인 것이다. 예를 들어, insertPerson
메소드에서 정상적으로 삽입을 해주지 않으면, selectPersonById
로 조회도 할 수 없을 것이기 때문이다.
selectPersonById
메소드를 위한 테스트 케이스인데 다른 메소드들의 상태에 따라 성공여부가 변하게 된다면, 단위 테스트(Unit Test)라는 의미에 명확하게 일치하지는 않는다고 할 수 있을 것이다.
이러한 문제점을 해결할 수 있는데 도움을 줄 수 있는 라이브러리가 바로 DbUnit이다.
그래서 DbUnit이 어떻게 도움을 줄 수 있는지 알아보고 기본적인 사용법도 정리해보도록 하겠다.
DbUnit의 역할을 간단하게 설명하면, DbUnit은 DAO를 대신해서 데이터를 삽입, 조회, 수정, 삭제하는 것이라고 할 수 있다.
그래서 select류의 메소드 테스트 전에 DAO를 대신하여 데이터를 삽입하여 테스트 코드에 맞는 데이터베이스 상태를 만들어 줄 수 있고,
insert류의 메소드 테스트에서는 insert후에 DAO를 대신하여 데이터를 조회하여 올바르게 삽입됐는지 확인해 줄 수 있다.
테스트 대상 메소드 외의 다른 메소드들에 의존하는 대신, DbUnit이라는 검증된 라이브러리에 의존하게 하는 것이다.
기본적인 개념에 대해 설명했으니 기본적인 사용법에 대해서 알아보자.
기본적인 사용법
DbUnit에서 데이터를 CRUD(삽입, 조회, 수정, 삭제)하기 위해서 데이터를 DbUnit에게 전달해야 하는데, 이 데이터들의 형식은 한가지는 아니고 여러가지 형식을 지원하고 있다.
대표적으로 xml이나 csv(comma separated value) 파일을 예로 들 수 있고, 이 파일을 DbUnit에서는 데이터셋(DataSet)이라고 한다.
그래서 다음과 같은 Person 테이블의 내용이 있다고 할 때,
ID | NAME | |
happy | john | john@geemail.com |
sad | mary | mary@never.com |
위의 내용은 xml 데이터셋으로 다음과 같이 표현된다.
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<Person ID="happy" NAME="john" EMAIL="john@geemail.com " />
<Person ID="happy" NAME="john" EMAIL="mary@never.com " />
</dataset>
그러면 이 데이터를 어떻게 데이터베이스에 삽입할 수 있는지 예제를 통해 알아보자.
public class PersonDAOImplTest {
private final String driver = "oracle.jdbc.driver.OracleDriver";
private final String url = "jdbc:oracle:thin:@16.34.131.73:1522:MAY";
private final String username = "may";
private final String password = "may1234";
private final String schema = "may";
private IDatabaseTester databaseTester;
@Autowired
private PersonDAO personDAO;
@Before
public void before() throws Exception {
databaseTester = new JdbcDatabaseTester(driver, url, username, password, schema);
IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File("src/test/resources/dataset/person.xml"));
DatabaseOperation.CLEAN_INSERT.execute(databaseTester.getConnection(), dataSet);
}
}
IDatabaseTest
라는 인터페이스에 JdbcDatabaseTest
라는 구현체로 JDBC 연결 방식을 이용하도록 했다.
그리고 @Before
어노테이션이 붙은 before 메소드에서 DB와 관련된 작업을 해주고 있다.
IDatabase
인터페이스에 JdbcDatabaseTest
구현체를 할당해주고, 이 때 생성자의 인자로 사용할 드라이버, 접속경로, 아이디, 비밀번호, 그리고 스키마를 넣어준다.
여기서 스키마는 테스트할 데이터베이스 스키마로, 계정명을 넣어준다고 생각하면된다.
그리고 나서 삽입할 데이터셋을 만드는데, xml 파일을 통해서 데이터셋을 만들 수 있는 FlatXmlDataSetBuilder
를 사용했다.
이 클래스의 build메소드에 xml File 객체를 인자로 전달하면 데이터셋을 만들 수 있다.
그리고 DatabaseOperation클래스의 CLEAN_INSERT를 통해 데이터를 삽입하는데, 인자로는 데이터베이스의 커넥션 정보와 데이터셋이다.
여기서 CLEAN_INSERT는 작업의 종류인데, 해당 테이블의 내용을 모두 지운뒤, 새롭게 삽입하라는 의미로 내부적으로 DELETE_ALL과 INSERT를 연속적으로 수행한 것이다.
몇 가지에 대해 살펴보면 다음과 같다.
- INSERT : 데이터셋 내용을 삽입한다. PK 기준으로 대상 테이블에 중복 데이터가 들어있지 않다는 가정하에서 동작하기 때문에 중복 데이터 존재 시 실패로 간주된다.
- DELETE_ALL : 데이터셋에 지정된 테이블들을 모두 지운다.
- REFLESH : 대상 테이블에 존재하지 않는 데이터는 INSERT, 이미 존재하는 데이터일 경우에는 UPDATE 한다. 이미 테이블에 존재하는 데이터는 건드리지 않는다.
- CompositeOperation : 여러 개의 DatabaseOperation을 하나로 묶어서 한 번에 실행한다.
// CompositeOperation example
DatabaseOperation op = new CompositeOperation( DatabaseOperation.DELETE_ALL, DatabaseOperation.INSERT);
op.execute(connection, xmlDataSet);
다시 예제로 돌아가서 설명하면, before는 매 테스크 케이스마다 실행되기 때문에, 모든 테스트 케이스 전에 항상 동일한 데이터베이스 상태를 유지할 수 있다.
이렇게 데이터 설정을 한 후에는 그 데이터들을 기반으로 테스트 케이스를 작성하는 일만 남았다.
그러면 DbUnit을 활용한 테스트 케이스 작성 예제를 몇 가지 제시해 보도록 하겠다.
먼저 insert 류의 메소드에 대한 테스트이다.
@Test
public void testInsertPerson() {
Person person = new Person();
person.setId("happy");
person.setName("tom");
person.setEmail("tom88@geemail.com");
personDAO.insertPerson(person);
ITable actualTable = databaseTester.getConnection().createDataSet().getTable("person");
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(new File("src/test/resources/dataset/person_insert.xml"));
ITable expectedTable = expectedDataSet.getTable("person");
Assertion.assertEquals(expectedTable, actualTable);
}
Person 객체를 삽입한 뒤, databaseTester를 이용해 실제 DB에서 person 테이블의 내용을 조회한다. 그리고 조회한 내용은 ITable 이라는 인터페이스에 담는다. ITable은 하나의 테이블에 대한 내용은 담는다고 보면 된다.
그리고 expectedTable 변수에 테이블 내용을 하나 더 담는데, 내용은 xml형식의 데이터셋을 통해 담는다.
여기서 이 person_insert.xml 파일은 초기 상태의 person 테이블의 상태에다가 새로 insert한 내용이 추가된 내용이 저장되있어야 한다. insert 후에 예상되는 person 테이블의 상태를 우리가 직접 작성해 놓으면 된다.
그래서 DAO가 정상적으로 person 객체를 삽입했다면 actualTable과 expectedTable의 내용은 같다고 예상해볼 수 있다.
비교를 위해서 Assertion.assertEquals메소드를 통해서 두 개의 테이블을 비교한다. Assertion 클래스는 JUnit에서 제공하는 클래스는 아니고 DbUnit에서 테이블 비교를 위해서 제공하는 클래스이다.
Assertion 클래스에서는 다른 유용한 메소드들도 제공하는데 그 중에서 한 가지를 소개하자면..
Assertion.assertEqualsIgnoreCols(ITable expectedTable, ITable actualTable, String[] ignoreCols)
assertEquals 메소드와 마찬가지로 테이블 내용을 비교하는데 ignoreCols로 전달한 컬럼들의 내용은 달라도 된다고 명시하는 것이다.
배열이기 때문에 여러 개의 컬럼을 전달할 수 있다.
이 메소드가 유용하게 쓰일 수 있는 경우는, 테이블에 값이 시퀀스를 이용하는 컬럼이 있는 경우나 오라클의 SYSDATE 처럼 삽입하기 전까지 정확한 값을 알 수 없는 경우이다. DB에 삽입 시 삽입될 값을 xml파일에 미리 명시 해놓을 수 없기 때문이다.
update, delete류의 메소드도 insert와 유사한 방식으로 작성할 수 있다.
select류 메소드의 경우에는 단건 select는 미리 DbUnit을 통해 설정해놓은 데이터를 조회한 뒤 제대로 가져왔는지 검사하면되는데,
목록을 select하는 메소드의 경우에는, List 객체를 ITable 형태로 만들거나 ITable 객체를 List 형태로 만들 수 있는 메소드가 없어서, 직접 예상되는 List 객체를 만들어서 비교하던지, 갯수만 비교하는 방법으로 수행하면 될 것이다.
정리
마지막으로 정리하자면, DbUnit은 테스트 간의 종속 관계나 DB 상태에 의존하지 않는 테스트를 짜는데 도움을 주는 라이브러리이다. 하지만 데이터셋의 관리에 추가적인 비용이 소모되기 때문에 모든 부분에 적용하기는 힘들고 필요하다고 생각되는 부분(예, 핵심적인 기능을 수행하는 DAO)에 선택적으로 적용하는 것이 좋을 것이다.
참고자료 : 책-TDD 실천법과 도구