테스트주도개발 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 처리를 잘 하자.
  • 데이터셋 크기는 작게하고 여러개 만들어라. 꼭 필요한 테스트 데이터 위주로 만들자
  • 데이터셋을 너무 만들지 마라. 유지보수가 힘들다
  • 데이터셋은 테스트 클래스 기반으로 만들고 여러 테스트 클래스와 공유해서 사용하지 말자
  • 테스트용 데이터베이스에서는 참조키나 널값 제약 기능을 꺼놓으면 편리하다.

Tags:

Categories:

Updated: