테스트주도개발 06
Updated:
테스트 주도개발 6장
- 오픈 소스 중 하나인 Unitils(유닛틸즈)는 단위 테스트를 좀 더 쉽게 만들고 더 유연하게 유지할 수 있게 도와주는 일종의 단위 테스트 지원 라이브러리다.
- DBUnit과 같이 독립적이 아닌 다른 테스트 프레임워크와 같이 사용된다.
6.1 설치
- 다운 받고 경로 추가
6.2 Unitils의 단위 테스트 지원 기능들
6.2.1 객체 동치성 비교
동일성 vs 동치성
-
동일성은 같은 객체인가 판단하는 것
-
동치성은 객체가 표현하고자 하는 상태(속성 값)가 서로 일치하는가 따져보는 것
-
예시로 바로 보자
public class Book {
private String name;
private String author;
private int price;
public Book(String name, String author, int price) {
this.name = name;
this.author = author;
this.price = price;
}
}
- 위 Book 객체를 비교하는 테스트 케이스
@Test
public void testBook() throws Exception {
Book aBook = new Book("사람은 무엇으로 사는가?","톨스토이", 9000);
Book otherBook = new Book("사람은 무엇으로 사는가?","톨스토이", 9000);
assertEquals(aBook, otherBook);
}
- 위의 테스트를 실행하면 다음과 같이 오류가 발생한다.
Java.lang.AssertionError: expected:<main.Book@16f0472> but was:<main.Book@18d107f>
at org.junit.Assert.fail(Assert.Java:91)
- 이는 의도하는 객체의 상태는 같으나 결과는 같은 객체가 다르다고 판정.
-
따라서 원하는 객체의 상태를 비교하기 위해 필드 값을 비교해야한다.
-
하지만 필드가 많아지만 아래와 같이 다 비교하기에는 불편하다.
- 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 testBook() throws Exception {
Book aBook = new Book("사람은 무엇으로 사는가?","톨스토이", 9000);
Book otherBook = new Book("사람은 무엇으로 사는가?","톨스토이", 9000);
assertEquals(aBook.getName(), otherBook.getName());
assertEquals(aBook.getAuthor(), otherBook.getAuthor());
assertEquals(aBook.getPrice(), otherBook.getPrice());
}
- 그래서 Unitils는 이런 불편함을 감소하기 위해 리플렉션 단정문이라는 기능을 제공
6.2.2 리플렉션 단정문(Reflection Assertion)
assertReflectionEquals(예상 객체, 실제 객체);
assertReflectionEquals([메시지], 예상 객체, 실제 객체);
- 형식은 assertEquals와 같다.
- 아래는 예시
import static org.unitils.reflectionassert.ReflectionAssert.*;
public class BookTest {
@Test
public void testBook() throws Exception {
Book aBook = new Book("사람은 무엇으로 사는가?","톨스토이", 9000);
Book otherBook = new Book("사람은 무엇으로 사는가?","톨스토이", 9000);
assertReflectionEquals("Book 객체 필드 비교", aBook, otherBook);
}
}
- 단정문에서 몇 가지 옵션을 지정할 수 있다.
- 대부분 소스코드가 리펙토링이 일어나면서 발생하는 테스트 케이스의 깨짐을 보완하기 위해 사용.
- 이 방식을 너그러운 단정문 적용이라고 부른다.
6.2.3 리플렉션 단정문의 너그러운 비교(Lenient Assertion)
assertReflectionEquals(예상 객체, 실제 객체, ReflectionComparatorMode);
ReflectionComparatorMode 옵션
ReflectionComparatorMode | 설명 |
---|---|
LENIENT_ORDER | 컬렉션이나 배열 비교시 순서 무시 |
IGNORE_DEFAULTS | 예상 객체 필드 중 타입 기본값을 갖는 필드에 대해서 비교하지 않음 |
LENIENT_DATES | 시간, 날짜 타입은 비교 안함 |
6.2.3.1 LENIENT_ORDER
- 예시를 보자
List<Integer> myList = Arrays.asList(3, 2, 1);
assertReflectionEquals(Arrays.asList(1, 2, 3), myList, LENIENT_ORDER);
6.2.3.2 IGNORE_DEFAULTS
- Java에서 int의 0, 객체의 null, boolean의 false은 각 타입의 기본 값이다.
- IGNORE_DEFAULTS은 필드 값에 같은 타입 기본값이 할당 되어 있으면 동치성 비교에 포함시키지 않는다.
-
이때 기준은 expected 객체를 기준으로 한다.
- 예시를 보자
Item expectedItem = new Item("IKH-001", null, 24000);
Item actualItem = new Item("IKH-001", "20040601", 24000);
assertReflectionEquals(expectedItem, actualItem, IGNORE_DEFAULTS);
// null이 만약 actualItem에 있었다면 오류가 발생
6.2.3.3 LENIENT_DATES
- 예시를 보자.
Item expectedItem = new Item("IKH-001", null, 24,new Date(System.currentTimeMillis()+1));
Item actualItem = new Item("IKH-001", null, 24000, new Date(System.currentTimeMillis()));
assertReflectionEquals(expectedItem, actualItem, LENIENT_DATES);
- Unitiles에는 assertLenientEquals를 제공한다.
- ReflectionComparatorMode의 assertReflectionEquals 간략화 버전
- 아래는 위의 세 개의 옵션 중 LENIENT_DATES를 제외한 두 개를 동시에 적용해서 비교
List<Integer> bag = Arrays.asList(100, 200, 300);
assertLenientEquals(Arrays.asList(300, 200, 100), bag);
// 배열의 순서가 다른 경우
assertLenientEquals(new String[]{"a", "B", "c"}, new String[]{"B","c","a"});
// 필드값이 타입 기본값일 경우 비교에서 제외
Item expectedItem = new Item("IKH-001", null, 24000);
Item actualItem = new Item("IKH-001", "20040601", 24000);
assertLenientEquals(expectedItem, actualItem);
//위 모두 성공
6.2.4 프로퍼티 단정문(Property Assertions)
- 객체의 필드를 비교하려면 getter 메소드로 직접 확인하면 된다.
- 그러나 getter가 없다면? 임시로 getter를 만들어도 되나 assertPropertyLenientEquals를 사용하자.
assertPropertyLenientEquals(속성 이름, 예상되는 속성 값, 실제 객체)
@Test
public void testPlayerPropertyTest() throws Exception {
Player player = VolleyballTeamRepository.getCaptain();
assertPropertyLenientEquals("age", 31, player);
assertPropertyLenientEquals("experienceYear", 15, player);
}
//age, experienceYear의 getter가 없다고 가정. 이를 위해 assertPropertyLenientEquals 사용
- 이것의 장점은 자바 빈 규칙을 따른다.
- 예를 들어 getter 메소드가 생기면 bean 규약에 맞는 getter 메소드를 이용해 프로퍼티 값을 불러와서 비교
6.3 Unitils 모듈
- Unitils는 DB, Hibernate, JPA, Spring, Mock Object에 대해서 유용한 기능을 모듈이라는 개념으로 제공
- 이를 사용하기 위해 Test Listener가 필요
- 상속 또는 @RunWith를 사용하여 Test Runner로 지정하면 됨
//JUnit4의 사용법
import org.junit.runner.RunWith;
import org.unitils.UnitilsJUnit4TestClassRunner;
@RunWith(UnitilsJUnit4TestClassRunner.class)
public class MyTest {
}
- 아래의 소스를 기반으로 설명할 것이다.
public class RepositoryTest {
private final String driver = "org.apache.derby.jdbc.EmbeddedDriver";
private final String protocol = "jdbc:derby:";
private final String dbName = "shopdb";
private IDatabaseTester databaseTester;
private IDatabaseConnection connection;
@Before
public void setUp() throws Exception{
databaseTester = new JdbcDatabaseTester(driver, protocol + dbName);
connection = databaseTester.getConnection();
IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File("seller.xml"));
DatabaseOperation.CLEAN_INSERT.execute(connection, dataSet);
}
@After
public void tearDown() throws Exception{
this.connection.close();
}
@Test
public void testFindById() throws Exception {
Repository repository = new DatabaseRepository();
Seller actualSeller = repository.findById("horichoi");
assertPropertyLenientEquals("id", "horichoi", actualSeller);
assertPropertyLenientEquals("name", "최승호", actualSeller);
assertPropertyLenientEquals("email", "megaseller@hatmail.com", actualSeller);
}
}
6.4 DbUnit과 함께 사용하는 데이터베이스 지원 모듈
환경 준비를 위한 unitils.properties, DatabaseRepositoryTest.xml 파일 설정
# unitils.properties
database.driverClassName=org.apache.derby.jdbc.EmbeddedDriver
database.url=jdbc:derby:shopdb
database.userName=
database.password=
database.schemaNames=APP
database.dialect=derby
DatabaseModule.Transactional.value.default=disabled
# Derby DB 임베디드 모드의 기본 스키마 이름은 APP
# database.dialect에는 Unitils에서 DB에 맞는 내부 SQL을 사용할 수 있게 DB 종류
# 아래는 오라클일 경우
database.driverClassName=oracle.jdbc.driver.OracleDriver
database.url=jdbc:oracle:thin:@yourmachine:1521:YOUR_DB
- 책에서의 예제는 자동커밋 기준으로 작성됐기에, Unitils가 트랜잭션 처리를 하지 않도록 disabled로 지정
- Unitils에서 DB 사용 시의 기본 트랙잭션 관리는 커밋
<!--> DatabaseRepositoryTest.xml </-->
<?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>
6.4.1 @DataSet
- 클래스이름.xml이라는 파일을 기 본 데이터셋으로 인식하고 DB로 읽어들임
- 이때 데이터셋 파일은 FlatXmlDataSet 타입
- DB로 읽어들일 때의 기본 동작은 모두 삭제하고 집어넣는 CLEAN_ INSERT
- 아래는 예제
@RunWith(UnitilsJUnit4TestClassRunner.class) // ➊
@DataSet // ➋
public class DatabaseRepositoryTest {
@Test
public void testFindById() throws Exception {
Repository repository = new DatabaseRepository();
Seller actualSeller = repository.findById("horichoi");
assertPropertyLenientEquals("id", "horichoi", actualSeller); // ➌
assertPropertyLenientEquals("name", "최승호", actualSeller);
assertPropertyLenientEquals("email", "megaseller@hatmail.com", actualSeller);
}
}
- 1 - Unitils 모듈 서비스를 이용하기 위해 Test Listener를 지정
- 2 - 클래스 레벨에서 @DataSet이 사용되면 클래스이름.xml 파일을 데이터셋으로 인식한다. 그래서 DatabaseRepositoryTest.xml을 읽어드린다.
-
3 - 비교
- 클래스이름과 데이터셋 파일 이름이 일치하지 않는다면 직접 지정 가능
- 또는 여러 개의 데이터셋 지정 가능
@DataSet("seller.xml")
@DataSet("seller.xml", "item.xml")
- 클래스가 아닌 메소드 레벨에서도 지정 가능
@Test
@DataSet("DatabaseRepositoryTest.testAddNewSeller.xml")
public void testAddNewSeller() throws Exception {
Seller newSeller = new Seller("hssm","이동욱","scala@hssm.kr");
Repository repository = new DatabaseRepository();
}
6.4.1.1 데이터셋 로드 전략
- 데이터셋 사용하는 동작에 여러가지가 있다. (위에는 CLEAN_INSERT가 사용)
- 두 가지 방식이 있다.
# unitils.properties에 설정한 경우
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy
// 애노테이션으로 지정한 경우
@DataSet(loadStrategy = InsertLoadStrategy.class)
- 아래는 종류. 자세한건 5장의 DbUnit의 DatabaseOperation 항목 보기
종류 | 설명 |
---|---|
CleanInsertLoadStrategy | 대상 테이블의 내용을 모두 지우고 데이터셋의 내용을 INSERT한다. |
InsertLoadStrategy | 데이터셋을 INSERT만 한다. |
RefreshLoadStrategy | 데이터셋의 내용으로 DB를 갱신한다. 이미 존재하는 데이터는 UPDATE, 없 는 데이터는 INSERT한다. |
UpdateLoadStrategy | DB에 존재하는 데이터를 UPDATE한다. |
6.4.2 @TestDataSource, 테스트에 사용하는 데이터소스 접근하기
- 지금까지는 설정파일에 지정된 DB 값을 이용해 DataSource를 구성해 자동으로 DB 연결을 만들어 테스트를 진행하도록 설계되었다.
- 그러나 경우에 따라서 데이터소스에 직접 접근할 필요가 있다.
- 이를 위해 두 가지 경우가 있다.
public class ShopDAOTest extends UnitilsJUnit4 {
@TestDataSource // - 1
private DataSource dataSource;
private ShopDAO dao
@Before
public void initialize () {
this.dao = new ShopDAO();
dao.setDataSource(dataSource);
//dao.setDataSource(DatabaseUnitils.getDataSource()); - 2
}
}
- 1 - 어노테이션 이용
- 2 - DatabaseUnitils.getDataSource() 메소드를 이용
6.4.3 @ExpectedDataSet, 예상 데이터셋을 이용한 테스트 메소드 레벨의 결과 비교
- 때로는 결과값을 비교할 때도 있음
- DbUnit에서는 예상값과 마찬가지로 데이터셋으로 만들어서 상태를 비교
- Unitils에서는 이럴 때 사용할 수 있도록 @ExpectedDataSet 제공
- 기존의 코드 예제
@Test
public void testAddNewSeller() throws Exception {
Seller newSeller = new Seller("hssm","이동욱","scala@hssm.kr");
Repository repository = new DatabaseRepository();
repository.add(newSeller);
ITable actualTable = connection.createQueryTable("SELLER", "select * from seller");
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(
new File("expected_seller.xml"));
ITable expectedTable = expectedDataSet.getTable("seller");
Assertion.assertEquals(expectedTable, actualTable);
}
- 아래는 @ExpectedDataSet 한 예제
@Test
@ExpectedDataSet("expected_seller.xml")
public void testAddNewSeller() throws Exception {
Seller newSeller = new Seller("hssm","이동욱","scala@hssm.kr");
Repository repository = new DatabaseRepository();
repository.add(newSeller);
}
- 직접 예상 데이터셋을 지정하지 않는다면 ‘‘클래스이름.메소드이름-result.xml’ 형식의 파일을 찾음
6.4.4 @Transactional, 트랜잭션 처리
트랜잭션이란
- 하나의 업무가 완결성을 갖고 정상처리되기 위해 필요한 하위 업무나 기능들의 집합.
- 중간에 하나라도 오류가 발생시 이전에 처리된 업무가 시작 시점으로 돌아감.
-
흔히 DB에서 DML 문장이 모두 성공하면 커밋, 실패하면 롤백의 상황을 지칭
- 기본적으로 Unitils의 DB 기능을 테스트에 수행하면 모든 테스트는 트랜잭션이 발생하는 상태로 동작하고 테스트 마지막에 커밋이 발생.
- 따라서 자신이 사용하는 DB가 트랜잭션을 지원하는지 봐야함
- 자신의 코드가 트랜잭션으로 동작해도 무방한지 고려해야함
- 적용하는 방법은 두 가지가 있다.
- 지정 가능 상태는 commit, rollback, disabled 세 가지가 있다.
- 위의 예제들은 자동 커밋 상태를 만들기 위해 Unitils의 트랜잭션 기능을 disable로 만들었다.
- 아래 예제는 테스트 케이스 수행 후 rollback처리 하도록 만듬.
# unitiils.properties 파일에 설정한 경우
DatabaseModule.Transactional.value.default=rollback
// @Transactional 애노테이션으로 지정한 경우
@Transactional(TransactionMode.ROLLBACK)
public class ShopDaoTest extends UnitilsJUnit4 {}
- Unitils는 트랜잭션 관련 기능을 구현하는 데 스프링 프레임워크의 트랜잭 션 관리 기능을 전적으로 차용해서 사용했음.
- 의존 라이브러리를 열어보면 spring 라이브러리들이 들어 있는 것을 확인할 수 있다.
6.5 DBMaintainer: DB를 자동으로 유지보수해주는 DB 유지보수 관리자
-
DBMaintainer는 개발자 각자의 DB 스키마를 SQL 스크립트를 이용해 자동 으로 유지시켜 주는 기능
-
규칙은 다음과 같다.
-
프로젝트 내에 폴더를 하나 만들어서 애플리케이션에서 필요한 DB스크립트를 넣어놓는다.
-
이때 DB스크립트는 숫자 형식의 버전넘버를 _(언더바)로 구분지어 갖도록 이름짓는다.
ex) 예: 001_DROP_ALL_TABLES.sql, 002_CREATE_TABLES.sql
-
Unitils의 DataSource를 이용하는 테스트 클래스를 실행한다.
-
DBMaintainer는 지정된 스크립트 폴더를 모니터링해서 변경된 내용이 있으면 반영한다
-
- DB 구조를 SQL 스크립트로 관리하고, 추가내용을 덧붙여 반영하도록 만들 때 매우 유용
- 스크립트 폴더에 변경분만 넣어주면 끝
- 모니터링 후 동작하는 방식은 두 가지
- 만일 새로운 스크립트가 추가된 경우 해당 스크립트만 실행
- 기존 스크립트가 변경된 경우 스키마를 전체 리셋하고 다시 처음부터 스크립트를 실행 (해당 유저의 스키마가 리셋된다.)
- 이때 번호가 매겨지지 않은 스크립트는 가장 나중에 실행된다.
- 필요에 따라 스크립트 폴더들도 번호를 붙여 순차적으로 실행되도록 만들 수 있다.
dbscripts/ 01_production/ 001_initial.sql
002_auditing_updates.sql
02_latest_dev/ 001_add_user_table.sql
002_rename_product_id.sql
6.5.1 DBMaintainer 기능 활성화시키기
- 프로퍼티에 아래와 같이 적용
# 프로젝트 홈 아래에 xsd라는 폴더를 만들어서 dataSetStructureGenerator 위치로 지정
dataSetStructureGenerator.xsd.dirName=xsd
# DBMaintainer 기능은 기본값으로는 false
# 적용 범위는 Unitils의 DataSource를 사용하는 모든 테스트 클래스
updateDataBaseSchema.enabled=true
# 스크립트 파일들이 존재하는 폴더를 지정
dbMaintainer.script.locations=src/dbscripts
# DBMaintainer가 스크립트들을 버전관리하기 위해 사용하는 테이블
# 최초에는 없기 때문에 자동으로 만들도록 지정
dbMaintainer.autoCreateExecutedScriptsTable=true
- D 단순히 스키마의 데이터를 업데이트하는 것과 다르다.
- 다음과 같은 기능을 제공
- 모든 참조키와 Not Null 제약조건을 비활성화(disable)시키기 때문에 INSERT 스크립트 실행을 편리하게 만들어준다.
- 시퀀스(sequence) 타입들을 높은 숫자로 업데이트시켜 주기 때문에, 테스트 수행 시에 넣게 되는 데이터들의 기본키(primary key) 값을 고정값으로 지정해도 충돌이 나지 않도록 도와준다(시퀀스가 PK 컬럼으로 쓰일 때).
- 데이터베이스의 구조를 XSD(XML Schema Definition) 파일로 만들어준다. 해당 파일은 이클립스에서 GUI 형태로 볼 수 있다.
프레임워크 VS 라이브러리
- 둘 다 재사용을 목적으로 사용
-
프레임워크는 복잡한 문제를 해결하기 위해 기본 개념적인 구조, 보통 툴이나 컴포넌트들의 집합체를 의미
- 라이브러리는 스포트웨어를 개발하기 위해 사용되는 클래스나 서브루틴들의 집합체
- 프레임워크가 더 큰 범위
- 프레임워크는 층을 갖고 라이브러리는 API만을 갖는다.
- 프레임워크는 층을 따른 데이터의 흐름(flow)이 있다.
- 라이브러리는 값의 IN/OUT에 가깝다.
- 따라서 구조를 층으로 나누어 설명하는 그림이 있으면 프레임워크.
- 개발자가 만든 코드를 시스템이 호출하면 프레임워크
- 개발자가 직접 API를 호출하면 라이브러리