테스트주도개발 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 스크립트를 이용해 자동 으로 유지시켜 주는 기능

  • 규칙은 다음과 같다.

    1. 프로젝트 내에 폴더를 하나 만들어서 애플리케이션에서 필요한 DB스크립트를 넣어놓는다.

    2. 이때 DB스크립트는 숫자 형식의 버전넘버를 _(언더바)로 구분지어 갖도록 이름짓는다.

      ex) 예: 001_DROP_ALL_TABLES.sql, 002_CREATE_TABLES.sql

    3. Unitils의 DataSource를 이용하는 테스트 클래스를 실행한다.

    4. 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 단순히 스키마의 데이터를 업데이트하는 것과 다르다.
  • 다음과 같은 기능을 제공
    1. 모든 참조키와 Not Null 제약조건을 비활성화(disable)시키기 때문에 INSERT 스크립트 실행을 편리하게 만들어준다.
    2. 시퀀스(sequence) 타입들을 높은 숫자로 업데이트시켜 주기 때문에, 테스트 수행 시에 넣게 되는 데이터들의 기본키(primary key) 값을 고정값으로 지정해도 충돌이 나지 않도록 도와준다(시퀀스가 PK 컬럼으로 쓰일 때).
    3. 데이터베이스의 구조를 XSD(XML Schema Definition) 파일로 만들어준다. 해당 파일은 이클립스에서 GUI 형태로 볼 수 있다.

프레임워크 VS 라이브러리

  • 둘 다 재사용을 목적으로 사용
  • 프레임워크는 복잡한 문제를 해결하기 위해 기본 개념적인 구조, 보통 툴이나 컴포넌트들의 집합체를 의미

  • 라이브러리는 스포트웨어를 개발하기 위해 사용되는 클래스나 서브루틴들의 집합체
  • 프레임워크가 더 큰 범위
  • 프레임워크는 층을 갖고 라이브러리는 API만을 갖는다.
  • 프레임워크는 층을 따른 데이터의 흐름(flow)이 있다.
  • 라이브러리는 값의 IN/OUT에 가깝다.
  • 따라서 구조를 층으로 나누어 설명하는 그림이 있으면 프레임워크.
  • 개발자가 만든 코드를 시스템이 호출하면 프레임워크
  • 개발자가 직접 API를 호출하면 라이브러리

Tags:

Categories:

Updated: