테스트주도개발 05
Updated:
테스트 주도개발 5장
- 연휴에 너무 놀았다.. 백수 주제에 ㅠㅠ 공부 열심히 하자.
5.1 DbUnit 장점
- 독립적인 데이터 베이스 연결을 지원
- 데이터베이스의 특정 시점 상태를 쉽게 내보내거나 읽을 수 있다.
- 테이블이나 데이터셋을 서로 쉽게 비교할 수 있다.
*DbUnit은 프레임워크라기보다 라이브러리에 더 가깝다.
5.1.1 DB와 DBMS
- DB(데이터베이스)는 다루기 쉽도록 논리적인 구조로 저장된 데이터를 의미
- DBMS는 그런 DB를 관리해주는 것. 오라클, MSSQL 서버를 말함
- 그러나 이를 혼용해서 많이 씀. 오라클이 단순 DB다 라고 해도 무리는 없음.
5.2 데이터셋
- DB 안에서 존재하는 테이블 or 일부 xml, csv 파일로 나타낸 모습
- 여러 데이터셋 중 가장 대표적인 형식인 FlatXmlDataSet 예시
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<seller ID="horichoi" NAME="최승호" EMAIL="megaseller@hatmail.com"/>
<seller ID="buymore" NAME="김용진" EMAIL="shopper@nineseller.com"/>
<seller ID="mattwhew" NAME="이종수" EMAIL="admin@maximumsale.net"/>
</dataset>
- seller라고 표시된 요소는 DB 테이블 이름에 해당
- ID, NAME, EMAIL 등의 속성명은 해당 테이블 컬럼
5.2.1 데이터베이스 연결과 테이블 초기화
- 판매자 정보를 DB에 저장하는 DatabaseRepository 클래스와 테스트 예시
public interface Repository {
public Seller findById(String id);
public void add(Seller seller);
public void update(Seller seller);
public void remove(Seller seller);
}
public class DatabaseRepositoryTest {
@Test
public void testFindById() throws Exception {
Seller expectedSeller = new Seller("horichoi","최승호","seller@hatmail.com");
Repository repository = new DatabaseRepository();
Seller actualSeller = repository.findById("horichoi");
assertEquals(expectedSeller.getId(),actualSeller.getId());
assertEquals(expectedSeller.getName(),actualSeller.getName());
assertEquals(expectedSeller.getEmail(),actualSeller.getEmail());
}
}
- 위 테스트 케이스는 단순 ID 조회 결과 값을 예상 결과와 비교하는 로직이다.
- 하지만 가정한 DB 내의 데이터가 변경 된다면 기능 구현은 문제가 없으나 테스트가 실패할 경우가 발생
-
현재 가정되어 있는 DB 안의 데이터 상태가 테스트 수행 전 가정했던 상태로 유지를 원함
- 그래서 다음과 같이 변경
테스트 관련 테이블 초기화 -> 테스트 케이스 수행
public class DatabaseRepositoryTest {
private final String driver = "org.apache.derby.jdbc.EmbeddedDriver";
private final String protocol = "jdbc:derby:";
private final String dbName = "shopdb";
private IDatabaseTester databaseTester; // ➊
@Before
public void setUp() throws Exception{
databaseTester = new JdbcDatabaseTester(driver, protocol + dbName); // ➋
try {
IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File("seller.xml")); // ➌
DatabaseOperation.CLEAN_INSERT.execute(databaseTester.getConnection(), dataSet); // ➍
} finally {
databaseTester.getConnection().close();
}
}
}
- IDatabaseTester를 이용하면 DBTestCase를 상속받지 않아도 됨. IDatabaseTester에 이미 DBTestCase가 상속되어 있음.
-
1- IDatabaseTester에는 DB 연결과 데이터셋 관련 기능이 정의
- 2- DbUnit에서 제공하는 4개의 구현체
구현체 | 설명 |
---|---|
JdbcDatabaseTester | DriverManager를 이용해 DB 커넥션을 생성 |
PropertiesBasedJdbcDatabaseTester | DriverManager를 이용해 DB 커넥션을 생성. 단, 연결 설정은 시스템 프로퍼티로부터 읽어들인다 |
DataSourceDatabaseTester | javax.sql.DataSource를 이용해 DB 커넥션을 생성 |
JndiDatabaseTester | JNDI를 이용해 DataSource를 가져온다. |
- 3- 데이터셋 지정
- 4- DB 커넥션과 데이터셋을 이용해 DB에 특정 작업을 수행.
- DatabaseOperation 에 여러 종류가 있음
- 그 중에 CLEAN_INSERT는 데이터셋에 지정된 DB 테이블의 내용을 모두 지운 다음 데이터셋에 들어 있는 값으로 채워 넣는다.
- 의미적으로 DatabaseOperation 의 DELETE_ALL과 INSERT 두 동작을 연속으로 수행한 것과 동일
전체 해석
- 위의 setUp 메소드는 테스트 메소드가 수행되기 전에 항상 seller.xml에 지정된 상태로 테이블 초기화
5.2.2 테이터셋 비교
@Test
public void testAddNewSeller() throws Exception {
Seller newSeller = new Seller("hssm","이동욱","scala@hssm.kr");
Repository repository = new DatabaseRepository();
repository.add(newSeller); // 새로운 판매자 추가
Seller sellerFromRepository = repository.findById("hssm");
assertEquals(newSeller.getId(),sellerFromRepository.getId());
assertEquals(newSeller.getName(),sellerFromRepository.getName());
assertEquals(newSeller.getEmail(),sellerFromRepository.getEmail());
}
- 판매자를 추가하는 기능을 구현하는 메소드를 검증하기 위한 테스트 코드
- 크게 문제가 되지는 않으나 add 기능을 확인하기 위해 조회 기능이 있는 메소드(findById)를 써야한다.
- 이때 findById메소드가 수정되는 경우 문제가 발생할 수도 있다.
**테스트 케이스를 작성할 때에는 다른 부분에서 영향받는 부분이 최소화되어야 한다.
- DbUnit의 테이블 비교 기능을 이용해 다시 작성한 테스트 메소드
@Test
public void testAddNewSeller() throws Exception {
Seller newSeller = new Seller("hssm","이동욱","scala@hssm.kr");
Repository repository = new DatabaseRepository();
repository.add(newSeller);
IDataSet currentDBdataSet = databaseTester.getConnection().createDataSet(); // ➊
ITable actualTable = currentDBdataSet.getTable("seller"); // ➋
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(new File("expected_seller.xml")); // ➌
ITable expectedTable = expectedDataSet.getTable("seller"); // ➍
Assertion.assertEquals(expectedTable, actualTable); // ➎
}
- 1- createDataSet에 파라미터로 테이블 이름을 지정할 수 있음. 지정하지 않으면 전체 테이블과 데이터를 데이터셋으로 만듬
- 2 - 데이터셋에서 특정 테이블(seller)를 가져옴
- 3 - 미리 만들어 놓은 예상 테이블을 가져옴
- 4 - 예상 데이터셋 중에서 비교에 사용할 테이블을 읽어드림
-
5 - 비교
- 다음은 DbUnit에서 제공하는 메소드
public class Assertion {
public static void assertEquals(ITable expected, ITable actual)
public static void assertEquals(IDataSet expected, IDataSet actual)
}
- JUnit에서는 제공하지 않는 ITable 타입과 IDataSet 타입의 비교를 지원 해준다
IDataSet currentDBdataSet = databaseTester.getConnection().createDataSet(new String[]
{"seller"});
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(new File("expected_seller.xml"));
Assertion.assertEquals(expectedDataSet, currentDBdataSet);
- 위의 경우 데이터베이스의 스냅샷(Snapshot, 특정 시점의 상태나 이미지)을 잡아 테이블로 추출 할 때, 해당 테이블의 기본키(primary key, PK) 값으로 정렬한다.
- 그러나 데이터셋을 파일로부터 읽어들일 때는 해 당 테이블을 정렬하지 않는다.
- 그래서 만일 새로운 데이터가 테이블에 추가되어 PK 정렬로 인해 순서가 달라지면 동일한 내용의 데이터셋임에도 오류가 발생할 수 있다.
- 이럴 경우에는 SQL을 이용해 데이터셋의 일부만을 추출할 수 있게 도와주는 createQueryTable을 사용해서 직접 sql에 “ORDER BY” 구문을 포함시켜 버리거나, 아니면 파일 로부터 읽어들인 데이터셋 테이블을 SortedTable 클래스로 정렬한 다음 비교하면 된다.
// 자동 정렬이 일어나지 않도록 SQL 문을 지정하든가
ITable actualTable = connection.createQueryTable("seller", "select * from seller");
// 예상 결과 데이터셋 값을 정렬시켜 버리든가
Assertion.assertEquals(new SortedTable(expectedTable), actualTable);
5.3 DbUnit 데이터셋의 종류
-
데이터셋이란 개념은 하나의 타입을 나타냄과 동시에 테이블들의 집합체를 표현하는 IDataSet 인터페이스의 구현체를 의미
-
여러 종류가 있으나 책에 잘 사용하지 않는 거라고 쓰인 것은 안씀
5.3.1 FlatXmlDataSet
* 테이블 이름을 XML TAG 구성요소로 적는다.
* 컬럼 이름은 속성으로 적는다.
* 널(null)값을 넣을 컬럼은 표현하지 않는다. 자동으로 널값이 들어간다.
* XML DTD(Document Type Definitions)를 지정하지 않아도 된다.
* 데이터셋 중 가장 흔하게 사용된다.
- 위의 예제들에서 사용한 데이터셋
5.3.2 StreamingDataSet
* 데이터베이스의 커서(cursor) 개념처럼 단방향으로 동작하며 현재 레코드만 메모리에 존재한다.
* UPDATE, INSERT, REFRESH 같은 동작을 하는 XML 데이터셋을 읽어들일 때 매우 효율적으로 동작한다.
IDataSetProducer producer = new FlatXmlProducer(new InputSource("dataset.xml"));
IDataSet dataSet = new StreamingDataSet(producer);
5.3.3 DatabaseDataSet
* 데이터베이스 인스턴스에 대한 접근을 제공한다.
* 직접 new로 생성하지 않고 팩토리 메소드로 만들어낸다.
IDataSet currentDBdataSet = IDatabaseConnection.createDataSet();
5.3.4 XlsDataSet
* MS 엑셀 문서를 데이터셋으로 인식한다.
* 엑셀 문서 내의 각 시트(sheet)를 테이블로 인식한다.
* 시트의 첫 번째 줄을 컬럼 이름으로 인식한다.
* 나머지 줄은 데이터 값으로 인식한다.
5.3.5 ReplacementDataSet
* 테이터셋에서 특정한 문자열을 치환하기 위해 사용한다.
* 보통은 null 값을 다르게 표현하고 런타임 시에 치환하는 데 많이 사용한다.
<dataset>
<EMPLOYEE NO="101" NAME="안병현" EMAIL="megane@hssm.kr"/>
<EMPLOYEE NO="102" NAME="김상옥" EMAIL="[null]"/>
</dataset>
* 위와 같은 데이터셋 파일을 만들고 아래와 같이 코드로 읽어드려서 null 값을 변환
ReplacementDataSet dataSet = new ReplacementDataSet( new FlatXmlDataSet(……));
dataSet.addReplacementObject("[NULL]", null);
5.4 DbUnit의 DB 지원 기능
- 위에서 사용한 DatabaseOperation 클래스의 setUp 메소드 중 일부
public void setUp() throws Exception{
DatabaseOperation.CLEAN_INSERT.execute(databaseTester.getConnection(), dataSet);
}
형식 : DatabaseOperation.오퍼레이션이름.execute( DB커넥션, 데이터셋 );
오퍼레이션 의 종류 | 설명 |
---|---|
INSERT | * 데이터베이스에 데이터셋 내용을 INSERT * PK(primary key)를 기 준으로 대상 테이블에 중복 데이터가 들어 있지 않다는 가정하에서 동 작하기 때문에 중복 데이터가 존재하면 실패로 간주 * FK(foreign key, 참조키)가 걸려 있는 테이블의 경우 데이터셋의 순서에 따라 정상 적으로 INSERT가 안 될 수 있으므로 유의한다 |
DELETE_ALL | * 데이터셋에 지정된 테이블들의 데이터를 모두 지운다. 지정되지 않은 테이블들은 건드리지 않는다. |
CLEAN_INSERT | * 데이터셋에 지정된 테이블에 대해 DELETE_ALL을 수행한 다음, 데이 터셋에 있는 데이터 값을 INSERT * REFRESH와 함께 매우 잘 사용되는 기능이다. |
UPDATE | * 데이터셋의 내용으로 테이블을 업데이트 |
REFRESH | * CLEAN_INSERT와 함께 가장 많이 사용되는 기능 * 대상 테이블에 존 재하지 않는 데이터는 INSERT, 이미 존재하는 데이터일 경우에는 UPDATE한다. * 둘 다에 속하지 않는, 이미 테이블에 존재하는 데이터는 건드리지 않는다. |
DELETE | * 데이터셋과 일치하는 데이터를 테이블에서 지운다. 테이블 전체를 지 우지는 않는다. |
TRUNCATE | * 데이터셋에 지정된 테이블들의 데이터를 모두 지운다. * TRUNCATE는 DELETE와 달리 롤백(rollback)이 불가능하다. * 테이블 데이터 삭제 작 업은 데이터셋에 지정된 테이블 순서의 역순으로 적용된다. |
CompositeOperation | * 여러 개의 DatabaseOperation을 하나로 묶어서 한 번에 실행한다. * 이런 방식을 DatabaseOperation 클래스를 데코레이트 한다고 표현한다. DatabaseOperation op = new CompositeOperation( DatabaseOperation.DELETE_ALL, DatabaseOperation.INSERT); op.execute(connection, xmlDataSet); * 위 코드는 DELETE_ALL과 INSERT를 하나의 DatabaseOperation으로 묶은 모습이다. |
TransactionOperation | * 데이터셋을 처리할 때 트랜잭션으로 묶어서 처리할 것인지를 결정한 다. * DatabaseOperation 클래스를 데코레이트한다. DatabaseOperation op = new CompositeOperation( DatabaseOperation.DELETE_ALL, DatabaseOperation.INSERT); op = new TransactionOperation(operation); op.execute(connection, xmlDataSet); * 중간에 실패하면 전부 롤백 |
IdentityInsertOperation | * MS SQL 서버의 IDENTITY 컬럼을 잠시 비활성화시킨 상태로 만들어 INSERT 시에 오류가 발생하지 않도록 도와준다. * IDENTITY 컬럼은 자동으로 숫자가 증가해서 입력되는 컬럼으로, 특정 값을 강제로 넣는 것이 일반적인 INSERT 문으로는 불가능하다. |
데코레이트란?
- 대상이 되는 객체를 감싸서 객체의 기능을 확장하는 기법
- 뭔소리지? 이해불가.
5.5장은 Ant로 예를 드는데 요즘은 안쓰므로 gradle로 쓰는 것을 찾아보자
- 따로 이는 포스팅하자
5.6 정리
- DbUnit을 사용하는 것은 어렵지 않다.
- 단지 만드는 비용과 노력이 많이 든다.
- 따라서 DB를 사용할 때 반드시 사용할지 고민할 필요가 있다.
- 단순히 SQL 파일을 테스트 전후로 실행하는 편이 더 나을 수도 있다.
5.6.1 권장 사용법
- 개발자마다 데이터베이스 인스턴스나 스키마를 하나씩 쓸 수 있게 하라.
- 나중에 정리(tearDown)할 필요가 없도록 setUp 처리를 잘 하자.
- 데이터셋 크기는 작게하고 여러개 만들어라. 꼭 필요한 테스트 데이터 위주로 만들자
- 데이터셋을 너무 만들지 마라. 유지보수가 힘들다
- 데이터셋은 테스트 클래스 기반으로 만들고 여러 테스트 클래스와 공유해서 사용하지 말자
- 테스트용 데이터베이스에서는 참조키나 널값 제약 기능을 꺼놓으면 편리하다.