E ective Unit Testing
1 / 32
목차
1. 테스트의 가치
2. 테스트와 설계
3. 깨끗한 테스트
4. 테스트 불가 원인
5. 테스트 가능 설계를 위한 지침
6. 제 2의 JVM 언어를 활용한 테스트 작성
2 / 32
테스트의가치
테스트는 실수를 바로 잡아준다
빠른 피드백
테스트는 실사용에 적합한 설계를 끌어내 준다
테스트는 원하는 동작을 명확히 알려주어 군더더기를 없애준다
테스트 작성을 함으로써 호출 가능하고, 테스트 가능한 프로그램을 설계하게
된다 (테스트하기 어려울 정도의 복잡한 코드를 만드는걸 또는 테스트 하기
어려워진 로직의 코드를 만드는걸 미연에 방지한다)
테스트를 작성해서 얻게 되는 가장 큰 수확은 테스트 자체가 아니다, 작성 과정에
서 얻는 깨달음이다
테스트는 설계의 수단이나 품질 보증의 수단으로 이용될 수 있다.
작고 구체화된 요구사항(단위 테스트)을 만족하는지를 테스트코드로 검증하
여 설계가 올바른지 검증한다(설계를 간결하게 목적에 딱 맞게 유지)
테스트는 문서화의 한 형태로 기능할 수 있다.
어떤 함수를 호출하거나, 어떤 객체를 생성하는 방법을 알고 싶을 때, 테스트
코드는 예제로서의 역할을 할 수 있고, 이 문서는 컴파일 및 실행이 가능하며
항상 최신 상태를 유지하고 거짓을 보여줄 수 없다 (자동화 테스팅 환경 구성
시)
3 / 32
테스트는 생산성에 영향을 준다.
실수를 방지, 로직이 정확하게 동작하는지/사이드 이펙트가 없는지 보장
코드 커버리지 100%에 집착하지 마라 : 결함이 없음을 보장한다는 의미가
아니다.
테스트의 가치는 테스트가 확인하지 못한 코드가 어떤 것인가와 테스트가 프로그래밍
실수를 얼마나 정확하게 잡아내는가에 달려있다.
4 / 32
테스트가생산성에영향을미치는요소
5 / 32
테스트와설계
테스트를 작성하게 되면 호출자 관점에서 바라보기 때문에 프로그래머는 편리하게 호
출할 수 있는 소프트웨어를 설계할 수 있다. (소프트웨어가 호출 가능하고 테스트 가
능해지려면 주위 환경에서 분리되어야 한다, 테스트 작성은 이를 돕는다.)
테스트가능설계란?
테스트 가능 설계의 가장 큰 의의는 당연히 코드를 더 잘 테스트할 수 있도록 해준다는
것이다.
"제품 코드는 단위 테스트를 쉽고 빠르게 작성할 수 있도록 설계해야 한다"
테스트 코드에서 클래스를 생성하고, 구현 일부를 대체하고, 다른 시나리오를 시
뮬레이션 하고, 원하는 실행 경로를 선택하는 등의 작업을 쉽게 할 수 있도록 해야
한다.
6 / 32
깨끗한테스트
Fast(빠르게)
테스트는 빨리 돌아야 한다, 느리면 자주 돌릴 엄두도 낼 수 없고 피드백 주기
도 길어진다.
Independent(독립적으로)
각 테스트는 서로 의존 하면 안된다. 한 테스트가 다음 테스트의 실행 환경을
준비하면 안된다. 각 테스트는 독립적으로 어떤 순서로도 실행될 수 있어야
한다.
서로에게 의존하는 테스트가 만들어지면 앞의 테스트가 실패하게 되면 뒤의
테스트가 잇달아 실패하여 원인을 결함을 찾기 어려워진다.
Repeatable(반복가능하게)
테스트는 어떤 환경에서도 반복 가능해야 한다. 특정 환경이 지원 되지 않다
보면 테스트가 실패한 이유를 둘러댈 변명이 생기거나, 테스트를 수행 하지
못하는 상황에 직면한다.
Self-Validating(자가 검증하는)
테스트는 bool 값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를
알리고 로그를 읽게 만들어선 안된다. 그 경우 판단은 주관적이 되며 지루한
수작업 평가가 필요하게 된다.
Timely(적시에)
단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.(테스
트 주도 개발) 그렇지 않으면 테스트가 불가능한 코드나 테스트 하기 너무 여
러운 코드가 만들어질지 모른다. 7 / 32
테스트불가원인
클래스 생성 불가
메서드 호출 불가
결과 확인 불가
협력 객체 대체 불가
메서드 오버라이딩 불가
8 / 32
클래스생성불가
public class DocumentRepository {
private static final String API_KEY = "d869db7fe62fb07c";
private static String sessionToken;
static {
String serverHostName = System.getenv("ACL_SERVER_HOST");
SessionClient api = new SessionClientImpl(serverHostName);
sessionToken = api.openSession(API_KEY);
}
public DocumentRepository() {
...
}
}
테스트 작성자가 제일 처음 해야 할 일 중 하나는 바로 객체 생성이다. 근데 위와 같은
코드는 객체 생성을 할 수가 없다.
정적 초기화 블록에 동작 환경 구성이 하드코딩 되어 있어 객체 생성을 할 수 없고, 의
존성을 주입할 수 없다.
9 / 32
메서드호출불가
private void doSomethingByObject(Object obj) {
}
private void doSomethingByMap(Map<String, Object> paramMap) {
}
너무 보수적인 접근 제한자(private 메서드)
private 메서드는 테스트 하기 어렵다 (꼭 테스트 해야 한다면 리플렉션을 써
야할지도 모른다)
그루비의 경우 해당 클래스를 new를 통해 생성하는 테스트 하는 경우
private 메서드를 호출 할 수 있다(스프링을 통한 Autowired 빈은 안됨)
어떤 인자를 넣어야 할지 애매한 경우(예 : Object, Map)
직관적이지 않아 인자를 어떻게 구성하여야 할지 알 수 없다
10 / 32
결과확인불가
public void doSomething() {
// 이것도 저것도 요것도 그것도 다 하는데 성공/실패 여부 조차 알 수 없다
}
단위 테스트는 주로 반환 값을 확인하는데 결과가 올바른지 확인할 수 없다(void
메서드)
정 반환값이 필요 없는 경우 Exception 발생 여부 등의 검증을 넣어 줄 수도
있다
다른 협력객체와 상호 작용 하는 경우
근데 이 상호작용을 가로챌 수 없을때 의존성을 원하는 대로 주입할 수 없어
결과를 확인할 수 없다
A->B 이렇게 호출 및 반환이 이뤄지고 이에 따라 어떤 처리가 필요한 로
직이 있는데 B의 동작을 가로챌 수 없을때
예를 들면 B의 중요 동작들이 다 private 메서드로 있다고 할 경우,
협력 객체까진 생성하여 넘겼지만 private 메서드를 가로채지 못하
는 문제가 발생할 수 있다
11 / 32
협력객체대체불가
상호 작용이 잘 이루어지는지 확인해야 하는 협력 객체가 있는데, 그 객체를 생성
하는 로직이 하드코딩 되어 있는 경우 (협력 객체를 대체 할 수 없음)
getCollaborator().doStuff().askForStuff().doMoreStuff()
협력 객체를 대체할 수 있다하여도 그 과정이 쓸데 없이 복잡한 경우
예) 메서드 연쇄 호출 - 반환하는, 반환하는, 반환하는 ... 테스트를 만들어야
한다
12 / 32
메서드오버라이딩불가
private static final Collaborator getCollaborator() { ... }
위 코드는 대상 객체의 일부 코드만 변경하여 테스트 하고 싶은 환경을 만들 수 없
다.
위 코드는 private, final, static 때문에 아래와 같은 해당 메서드만을 Override
하여 테스트하는 걸 꿈도 꿀 수 없다.
@Test
public void test() {
final Collaborator collaborator = new TestDouble();
ObjectUnderTest o = new ObjectUnderTest() {
@Override
private static final getCollaborator() {
return collaborator;
}
};
...
}
굳이 해당 테스트를 원하면 다량의 리플렉션과 바이트 코드 조작으로 가능한데,
절대 추천할만한 방법이 아니다.
13 / 32
테스트가능설계를위한지침
복잡한 private 메서드를 피하라
final 메서드를 피하라
정적 메서드를 피하라
new는 신중하게 사용하라
생성장에서는 로직 구현을 피하라
싱글톤을 피하라
상속보다는 컴포지션을 사용하라
외부 라이브러리를 감싸라
서비스 호출을 피하라
14 / 32
복잡한private 메서드를피하라
private 메서드를 쉽게 테스트 하는 방법이란 없다.
이를 꼭 명심하고 애초에 private 메서드는 직접 테스트할 필요가 없도록 만
들어야 한다.
public 메서드의 가독성을 높이기 위한 간단한 유틸리티로 제한한다면
public 메서드만 테스트 해도 private 메서드까지 확실하게 검증된다.
// 단순 유틸리티성 메서드
private String makeMapKey(LocalDateTime localDateTime) {
return DateUtils.getFormattedDateStr(localDateTime, "yyyyMMddHH");
}
15 / 32
private 메서드 사용법이 명확치 않고 (직관적이지 않고 너무 다양한 일을 하는
복잡한 메서드라던지) 전용 테스트를 만들고 싶은 마음 까지 든다면 당당히
public 메서드로 제공하게 하자.
// 간단한 유틸리티성 코드라 보여질 수도 있지만, 정말 0~1시 사이의 시간을 정확히 검증되는지에
/**
* 0:00~0:59:59인지 확인
* @param startTime
* @return
*/
private boolean isBefore1AM(String startTime) {
LocalDateTime today = LocalDateTime.now();
String beginDateStr = DateUtils.getFormattedDateStr(today, "yyyy-MM-dd") + " 00
LocalDateTime beginDate = DateUtils.getLocalDateTimeFromStr(beginDateStr);
String endDateStr = DateUtils.getFormattedDateStr(today, "yyyy-MM-dd") + " 01:0
LocalDateTime endDate = DateUtils.getLocalDateTimeFromStr(endDateStr);
String startDateStr = DateUtils.getFormattedDateStr(today, "yyyy-MM-dd") + " "
LocalDateTime startDate = DateUtils.getLocalDateTimeFromStr(startDateStr);
if(beginDate.equals(startDate) || (startDate.isAfter(beginDate) && startDate.i
return true;
}
return false;
}
16 / 32
nal 메서드를피하라
public final void doNotOverride() {
}
final 메서드가 필요한 프로그램은 많지 않다. 대개는 해당 메서드의 오버라이드
를 막을 일이 많지 않을 것이다. (보수적인 접근 제한자 문제)
그러나 final의 경우 꼭 필요한 곳에 선언하면, 오버라이딩이 불가능하므로 컴파
일러가 메서드를 인라인해서 최적화할 수 있다. 그러나 단지 성능 때문 만에 메서
드를 final로 선언하는 경우는 거의 없을 것이다.
17 / 32
정적메서드를피하라
정적 메서드 대부분은 사실 정적 메서드가 아니었어야 한다. 소속을 결정하기 어
려워 단지 귀찮아서 유틸리티 클래스에 몰아 넣기도 한다.
스텁으로 만들고 싶은 메서드는 정적 메서드로 만들지 않는다.
// 주사윗면의 갯수를 인자로 호출하면 임의의 숫자를 반환한다.
// 임의성은 자동화된 테스트에서 피해야할 요소이니 rollDie() 메서드는 테스트에서 스텁으로 교체
// 예를 들면 이후 로직에선 반환값이 10 이상일때 이하일때에 따른 로직이 있다 가정한다면 임의성
public static int rollDie(int sides) {
return 1 + (int)(Math.random() * sides)
}
18 / 32
new는신중하게사용하라
public String createTagName(String topic) {
Timestamper c = new Timestamper();
return topic + c.timestamp;
}
객체를 new 하는 것은 정확한 구현이 그것이라고 못 박는 행위다. 하드코딩 되는
가장 흔한형태이다.
테스트 더블로 대체할 가능성이 없는 객체 생성에만 사용해야 한다.
new를 하드코딩하기 전에 질문해보라. 테스트할 때 그 객체를 다른 걸로 교체할
필요가 없을지? 필요가 있다면 메서드 안에서 생성할게 아니라 외부에서 넘겨받
게끔 해야한다.
19 / 32
생성자에서는로직구현을피하라
테스트에 영향을 미칠만한 로직을 생성자에 넣어선 안된다
위 코드는 "ipconfig"를 실행함으로 윈도우즈 컴퓨터에서만 생성할 수 있는 클래
스이다. 다른 운영체제에서도 실행할 수 있게 하려면 생성자의 로직을 메서드로
만들고 오버라이딩 할 수 있게 해주면 어떤 운영체제에서도 해당 객체를 생성하
는 테스트를 만들 수 있다.
// 너무 많은 일을 하는 생성자
public class UUID {
private String value;
public UUID() {
long macAddress = 0;
Process p = Runtime.getRuntime().exec(new String[] { "ipconfig", "/all" },
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStre
String line = null;
while (macAddress == null && (line = in.readLine()) != null) {
macAddress = extractMacAddressFrom(line);
}
...
}
}
20 / 32
싱글톤을피하라
public class Clock {
private static final Clock singleInstance = new Clock();
private Clock() { }
public static Clock getInstance() {
return singleInstance;
}
}
public class Log {
public void log(String message) {
String prefix = "[" + Clock.getInstance().timestamp() + "] ";
logFile.write(prefix + message);
}
}
싱글톤은 테스트가 자신에게 필요한 대용품을 만들 수 없게 가로막는다. Clock
객체는 한 번 초기화 되면 절대로 교체할 수 없다.
Clock 싱글톤을 사용하는 코드를 검사 할 때마다 상당히 번거롭게 된다.(Clock 객
체를 다른 객체로 변경하고 싶다면 리플렉션이나 별도의 setter가 필요하다)
만약 꼭 정적 싱글톤 메서드를 사용해야겠다면 getInstance() 메서드가 클래스
아닌 인터페이스를 반환하게 하도록 하는걸 추천한다 (???)
21 / 32
상속보다는컴포지션을사용하라
상속으로 인해 만들어진 클래스 계층 구조는 변경할 수 없으므로 테스트 용이성
을 떨어뜨린다.
기능을 재활용하기 위한 목적으로 상속을 이용한다면 컴포지션 방식이 낫다.
컴포지션은 상위 클래스를 호출할 일도 일절 없고, 상속의 상위 클래스의 메
서드 재사용과 달리 다른 구현을 통해 자숑할 수 있고, 실행 도중에도 마음껏
교체할 수 있다.
관련 링크 (자세한 내용은 아래 링크에)
Effective Java - 상속 대신 컴포지션 하라
외부라이브러리를감싸라
외부 라이브러리의 경우 코드에 대한 제어권이 우리에게 거의 없다.
테스틀 용이하게 하려면 직접 다른 구현으로 교체하기 쉽고 테스트하기도 편한
인터페이스를 하나 만들어서 그 라이브러리를 감싸자.
22 / 32
서비스호출을피하라
public class Assets {
public Collection<Asset> search(String... keywords) {
APIRequest searchRequest = createSearchRequestFrom(keywords);
APICredentials credentials = Configuration.getCredentials();
// setInstance 메서드를 만들면 객체를 바꾸는게 가능하긴 하다
APIClient api = APIClient.getInstance(credentials);
return api.search(searchReqeust);
}
...
}
서비스 호출 방식은 생성자 입력 방식보다 스텁으로 대체하기 훨씬 어렵다.
23 / 32
해당 코드로 테스트를 코드를 만들게 된다면
@Test
public void searchingByKeywords() {
final String[] keywords = { "one", "two", "three" };
final Collection<Asset> results = createListOfRandomAssets();
APIClient.setInstance(new FakeAPIClient(keywords, results));
Assets assets = new Assets();
assetEquals(results, assets.search(keywords));
}
이 경우 생성자 입력 방식을 쓰는게 오히려 다른 객체로 대체하기 쉽고 명시적이
기 때문에 협력 객체와의 관계가 더 직관적이다. (종속 객체를 직접 전달하라)
public class Assets {
private APIClient api;
public Assets(APIClient api) {
this.api = api;
}
...
}
24 / 32
제2의JVM 언어를활용한테스트작성
제 2의 JVM 언어를 사용하면 자바보다 더 간결하고 표현력이 좋다.
일반적인이점
자질구레한 문법이 적어 핵심 로직이 잘 드러남
더 읽기 쉬운 데이터 구조를 제공
표준 데이터 타입을 편하게 다룰 수 있는 메서드를 추가로 제공
더 강력한 기능을 언어 차원에서 지원
25 / 32
테스트작성
성능보다 가독성
테스트 코드 관점에선 성능은 치명적인 걸림돌이 아니다, 가독성이 더 중요
하다
제품 개발은 계속 자바로, 테스트 코드에만 제2의 JVM 언어를 사용
한정된 영역, 그것도 테스트에만 사용하면 되기에 새로운 언어를 익히고 적
용하는 부담도 크지 않다
테스트용 언어는 따로 있다
대부분의 언어는 애플리케이션이나 시스템 소프트웨어를 개발할 목적으로
설계된다(이는 테스트 작성엔 유용하지 않을 수 있다)
테스트 작성에는 대체로 간결한 문법과 다목적 데이터 구조를 제공하는
언어가 적합하다
가장 적합한 언어는 루비와 그루비다.
26 / 32
BDD 도구의뛰어난표현력
테스트의 의도를 더 명확하게 표현하기 위한 용어를 찾는 과정에서 탄생
이들 도구는 기대하는 동작과 테스트의 의도를 더 명확하게 드러내고 동시에
산만한 문법을 숨겨준다
BDD에 대한 간략한 설명
BDD는 시나리오를 기반으로 테스트 케이스를 작성하며, 개발자가 아닌 사
람이 봐도 이해할 수 있을 정도의 레벨을 권장한다.
하나의 시나리오는 아래와 같은 구조를 가진다
Feature : 테스트 대상의 기능/책임을 명시한다.
Scenario : 테스트 목적에 대한 상황을 설명한다.
Given : 시나리오 진행에 필요한 값을 설정한다.
When : 시나리오를 진행하는데 필요한 조건을 명시한다.
Then : 시나리오를 완료했을 때 보장해야 하는 결과를 명시한다.
코드예제
이 테스트 코드는 사실 정기 휴일, 휴무기간에 대한 테스트를 따로 만들었어야 되
는게 맞다.
27 / 32
@Title("휴무일 여부 테스트")
class StoreDayOffTest extends Specification {
@Unroll("#SOMEDAY일자는 휴무일 여부가 #EXPECTED여야 한다")
def "isDayOff"() {
given: "휴무일 정보 생성"
// 휴무일 정보
def storeDayOff = new StoreDayOff(
regularDaysOfWeek: REGULAR_DAYS_OF_WEEK,
temporaryDays: []
)
// 테스트 코드에 이런 IF는 적합하지 않은거 같다
if (START_DATE) {
storeDayOff.temporaryDays = [
new StoreDayOff.TemporaryDay(
startDate: LocalDate.parse(START_DATE),
endDate: LocalDate.parse(END_DATE)
)
]
}
when: "휴무일 여부 체크"
def result = storeDayOff.isDayOff(LocalDate.parse(SOMEDAY))
then: "검증"
EXPECTED == result
where: "데이터 테이블"
REGULAR_DAYS_OF_WEEK | START_DATE | END_DATE | SOMEDAY | EXPECTED
[ DayOfWeek.MONDAY ] | "" | "" | "2018-07-23" | true
[ DayOfWeek.MONDAY ] | "" | "" | "2018-07-24" | false
[] | "2018-07-01" | "2018-07-30" | "2018-07-23" | true
[] | "2018-07-01" | "2018-07-30" | "2018-08-01" | false
}
} 28 / 32
테스트실행결과
29 / 32
왜그루비로테스트를작성하는것이좋을까?
그루비 홈페이지에 설명된 장점중 일부 내용
makes modern programming features available to Java developers
with almost-zero learning curve
compiles straight to Java bytecode so you can use it anywhere you can
use Java
아래와 같은 링크에 나온 내용 처럼 간결한 문법을 이용한다면 표현력이 좋아지고(가
독성 향상) 테스트 코드를 만드는 생산성이 향상될 수 있다.
Groovy - Style Guide
Groovy - Differences with Java
Groovy - Working with a relational database
30 / 32
Spock
Spock의 기능 중 일부 소개하고 싶은 내용
Extension
@Ignore
메서드나 클래스 단위 테스트 무시
@IgnoreRest
다른 테스트 메서드는 다 무시하고 이를 설정한 해당 테스트만 실행
됨
@IgnoreIf
조건에 해당하는 경우 테스트 무시 (예:테스트 환경이 윈도우인 경
우)
@Requires
조건에 해당하는 경우 테스트 실행 (@IgnoreIf의 반대)
@PendingFeature
테스트를 실행하지만 실패시 무시한다 (개발중인 기능)
@Stepwise
순차적으로 테스트 메서드를 실행한다.
@Timeout
테스트 실행시간이 해당 시간이 지난 경우 테스트 실패로 간주
JUnit 과의 비교
데이터 테이블
데이터 테이블 사용시 시각적으로 기대 결과값을 표현하기 위해 '||'로 구분
31 / 32
Spock 관련 일부 소개하고 싶은 내용 링크
Spock - Comparison to Junit
Spock - Data Tables
Spock - Unroll
Spock - Syntatic Variations
Spock - Extensions
32 / 32

Effective Unit Testing

  • 1.
    E ective UnitTesting 1 / 32
  • 2.
    목차 1. 테스트의 가치 2.테스트와 설계 3. 깨끗한 테스트 4. 테스트 불가 원인 5. 테스트 가능 설계를 위한 지침 6. 제 2의 JVM 언어를 활용한 테스트 작성 2 / 32
  • 3.
    테스트의가치 테스트는 실수를 바로잡아준다 빠른 피드백 테스트는 실사용에 적합한 설계를 끌어내 준다 테스트는 원하는 동작을 명확히 알려주어 군더더기를 없애준다 테스트 작성을 함으로써 호출 가능하고, 테스트 가능한 프로그램을 설계하게 된다 (테스트하기 어려울 정도의 복잡한 코드를 만드는걸 또는 테스트 하기 어려워진 로직의 코드를 만드는걸 미연에 방지한다) 테스트를 작성해서 얻게 되는 가장 큰 수확은 테스트 자체가 아니다, 작성 과정에 서 얻는 깨달음이다 테스트는 설계의 수단이나 품질 보증의 수단으로 이용될 수 있다. 작고 구체화된 요구사항(단위 테스트)을 만족하는지를 테스트코드로 검증하 여 설계가 올바른지 검증한다(설계를 간결하게 목적에 딱 맞게 유지) 테스트는 문서화의 한 형태로 기능할 수 있다. 어떤 함수를 호출하거나, 어떤 객체를 생성하는 방법을 알고 싶을 때, 테스트 코드는 예제로서의 역할을 할 수 있고, 이 문서는 컴파일 및 실행이 가능하며 항상 최신 상태를 유지하고 거짓을 보여줄 수 없다 (자동화 테스팅 환경 구성 시) 3 / 32
  • 4.
    테스트는 생산성에 영향을준다. 실수를 방지, 로직이 정확하게 동작하는지/사이드 이펙트가 없는지 보장 코드 커버리지 100%에 집착하지 마라 : 결함이 없음을 보장한다는 의미가 아니다. 테스트의 가치는 테스트가 확인하지 못한 코드가 어떤 것인가와 테스트가 프로그래밍 실수를 얼마나 정확하게 잡아내는가에 달려있다. 4 / 32
  • 5.
  • 6.
    테스트와설계 테스트를 작성하게 되면호출자 관점에서 바라보기 때문에 프로그래머는 편리하게 호 출할 수 있는 소프트웨어를 설계할 수 있다. (소프트웨어가 호출 가능하고 테스트 가 능해지려면 주위 환경에서 분리되어야 한다, 테스트 작성은 이를 돕는다.) 테스트가능설계란? 테스트 가능 설계의 가장 큰 의의는 당연히 코드를 더 잘 테스트할 수 있도록 해준다는 것이다. "제품 코드는 단위 테스트를 쉽고 빠르게 작성할 수 있도록 설계해야 한다" 테스트 코드에서 클래스를 생성하고, 구현 일부를 대체하고, 다른 시나리오를 시 뮬레이션 하고, 원하는 실행 경로를 선택하는 등의 작업을 쉽게 할 수 있도록 해야 한다. 6 / 32
  • 7.
    깨끗한테스트 Fast(빠르게) 테스트는 빨리 돌아야한다, 느리면 자주 돌릴 엄두도 낼 수 없고 피드백 주기 도 길어진다. Independent(독립적으로) 각 테스트는 서로 의존 하면 안된다. 한 테스트가 다음 테스트의 실행 환경을 준비하면 안된다. 각 테스트는 독립적으로 어떤 순서로도 실행될 수 있어야 한다. 서로에게 의존하는 테스트가 만들어지면 앞의 테스트가 실패하게 되면 뒤의 테스트가 잇달아 실패하여 원인을 결함을 찾기 어려워진다. Repeatable(반복가능하게) 테스트는 어떤 환경에서도 반복 가능해야 한다. 특정 환경이 지원 되지 않다 보면 테스트가 실패한 이유를 둘러댈 변명이 생기거나, 테스트를 수행 하지 못하는 상황에 직면한다. Self-Validating(자가 검증하는) 테스트는 bool 값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를 알리고 로그를 읽게 만들어선 안된다. 그 경우 판단은 주관적이 되며 지루한 수작업 평가가 필요하게 된다. Timely(적시에) 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.(테스 트 주도 개발) 그렇지 않으면 테스트가 불가능한 코드나 테스트 하기 너무 여 러운 코드가 만들어질지 모른다. 7 / 32
  • 8.
    테스트불가원인 클래스 생성 불가 메서드호출 불가 결과 확인 불가 협력 객체 대체 불가 메서드 오버라이딩 불가 8 / 32
  • 9.
    클래스생성불가 public class DocumentRepository{ private static final String API_KEY = "d869db7fe62fb07c"; private static String sessionToken; static { String serverHostName = System.getenv("ACL_SERVER_HOST"); SessionClient api = new SessionClientImpl(serverHostName); sessionToken = api.openSession(API_KEY); } public DocumentRepository() { ... } } 테스트 작성자가 제일 처음 해야 할 일 중 하나는 바로 객체 생성이다. 근데 위와 같은 코드는 객체 생성을 할 수가 없다. 정적 초기화 블록에 동작 환경 구성이 하드코딩 되어 있어 객체 생성을 할 수 없고, 의 존성을 주입할 수 없다. 9 / 32
  • 10.
    메서드호출불가 private void doSomethingByObject(Objectobj) { } private void doSomethingByMap(Map<String, Object> paramMap) { } 너무 보수적인 접근 제한자(private 메서드) private 메서드는 테스트 하기 어렵다 (꼭 테스트 해야 한다면 리플렉션을 써 야할지도 모른다) 그루비의 경우 해당 클래스를 new를 통해 생성하는 테스트 하는 경우 private 메서드를 호출 할 수 있다(스프링을 통한 Autowired 빈은 안됨) 어떤 인자를 넣어야 할지 애매한 경우(예 : Object, Map) 직관적이지 않아 인자를 어떻게 구성하여야 할지 알 수 없다 10 / 32
  • 11.
    결과확인불가 public void doSomething(){ // 이것도 저것도 요것도 그것도 다 하는데 성공/실패 여부 조차 알 수 없다 } 단위 테스트는 주로 반환 값을 확인하는데 결과가 올바른지 확인할 수 없다(void 메서드) 정 반환값이 필요 없는 경우 Exception 발생 여부 등의 검증을 넣어 줄 수도 있다 다른 협력객체와 상호 작용 하는 경우 근데 이 상호작용을 가로챌 수 없을때 의존성을 원하는 대로 주입할 수 없어 결과를 확인할 수 없다 A->B 이렇게 호출 및 반환이 이뤄지고 이에 따라 어떤 처리가 필요한 로 직이 있는데 B의 동작을 가로챌 수 없을때 예를 들면 B의 중요 동작들이 다 private 메서드로 있다고 할 경우, 협력 객체까진 생성하여 넘겼지만 private 메서드를 가로채지 못하 는 문제가 발생할 수 있다 11 / 32
  • 12.
    협력객체대체불가 상호 작용이 잘이루어지는지 확인해야 하는 협력 객체가 있는데, 그 객체를 생성 하는 로직이 하드코딩 되어 있는 경우 (협력 객체를 대체 할 수 없음) getCollaborator().doStuff().askForStuff().doMoreStuff() 협력 객체를 대체할 수 있다하여도 그 과정이 쓸데 없이 복잡한 경우 예) 메서드 연쇄 호출 - 반환하는, 반환하는, 반환하는 ... 테스트를 만들어야 한다 12 / 32
  • 13.
    메서드오버라이딩불가 private static finalCollaborator getCollaborator() { ... } 위 코드는 대상 객체의 일부 코드만 변경하여 테스트 하고 싶은 환경을 만들 수 없 다. 위 코드는 private, final, static 때문에 아래와 같은 해당 메서드만을 Override 하여 테스트하는 걸 꿈도 꿀 수 없다. @Test public void test() { final Collaborator collaborator = new TestDouble(); ObjectUnderTest o = new ObjectUnderTest() { @Override private static final getCollaborator() { return collaborator; } }; ... } 굳이 해당 테스트를 원하면 다량의 리플렉션과 바이트 코드 조작으로 가능한데, 절대 추천할만한 방법이 아니다. 13 / 32
  • 14.
    테스트가능설계를위한지침 복잡한 private 메서드를피하라 final 메서드를 피하라 정적 메서드를 피하라 new는 신중하게 사용하라 생성장에서는 로직 구현을 피하라 싱글톤을 피하라 상속보다는 컴포지션을 사용하라 외부 라이브러리를 감싸라 서비스 호출을 피하라 14 / 32
  • 15.
    복잡한private 메서드를피하라 private 메서드를쉽게 테스트 하는 방법이란 없다. 이를 꼭 명심하고 애초에 private 메서드는 직접 테스트할 필요가 없도록 만 들어야 한다. public 메서드의 가독성을 높이기 위한 간단한 유틸리티로 제한한다면 public 메서드만 테스트 해도 private 메서드까지 확실하게 검증된다. // 단순 유틸리티성 메서드 private String makeMapKey(LocalDateTime localDateTime) { return DateUtils.getFormattedDateStr(localDateTime, "yyyyMMddHH"); } 15 / 32
  • 16.
    private 메서드 사용법이명확치 않고 (직관적이지 않고 너무 다양한 일을 하는 복잡한 메서드라던지) 전용 테스트를 만들고 싶은 마음 까지 든다면 당당히 public 메서드로 제공하게 하자. // 간단한 유틸리티성 코드라 보여질 수도 있지만, 정말 0~1시 사이의 시간을 정확히 검증되는지에 /** * 0:00~0:59:59인지 확인 * @param startTime * @return */ private boolean isBefore1AM(String startTime) { LocalDateTime today = LocalDateTime.now(); String beginDateStr = DateUtils.getFormattedDateStr(today, "yyyy-MM-dd") + " 00 LocalDateTime beginDate = DateUtils.getLocalDateTimeFromStr(beginDateStr); String endDateStr = DateUtils.getFormattedDateStr(today, "yyyy-MM-dd") + " 01:0 LocalDateTime endDate = DateUtils.getLocalDateTimeFromStr(endDateStr); String startDateStr = DateUtils.getFormattedDateStr(today, "yyyy-MM-dd") + " " LocalDateTime startDate = DateUtils.getLocalDateTimeFromStr(startDateStr); if(beginDate.equals(startDate) || (startDate.isAfter(beginDate) && startDate.i return true; } return false; } 16 / 32
  • 17.
    nal 메서드를피하라 public finalvoid doNotOverride() { } final 메서드가 필요한 프로그램은 많지 않다. 대개는 해당 메서드의 오버라이드 를 막을 일이 많지 않을 것이다. (보수적인 접근 제한자 문제) 그러나 final의 경우 꼭 필요한 곳에 선언하면, 오버라이딩이 불가능하므로 컴파 일러가 메서드를 인라인해서 최적화할 수 있다. 그러나 단지 성능 때문 만에 메서 드를 final로 선언하는 경우는 거의 없을 것이다. 17 / 32
  • 18.
    정적메서드를피하라 정적 메서드 대부분은사실 정적 메서드가 아니었어야 한다. 소속을 결정하기 어 려워 단지 귀찮아서 유틸리티 클래스에 몰아 넣기도 한다. 스텁으로 만들고 싶은 메서드는 정적 메서드로 만들지 않는다. // 주사윗면의 갯수를 인자로 호출하면 임의의 숫자를 반환한다. // 임의성은 자동화된 테스트에서 피해야할 요소이니 rollDie() 메서드는 테스트에서 스텁으로 교체 // 예를 들면 이후 로직에선 반환값이 10 이상일때 이하일때에 따른 로직이 있다 가정한다면 임의성 public static int rollDie(int sides) { return 1 + (int)(Math.random() * sides) } 18 / 32
  • 19.
    new는신중하게사용하라 public String createTagName(Stringtopic) { Timestamper c = new Timestamper(); return topic + c.timestamp; } 객체를 new 하는 것은 정확한 구현이 그것이라고 못 박는 행위다. 하드코딩 되는 가장 흔한형태이다. 테스트 더블로 대체할 가능성이 없는 객체 생성에만 사용해야 한다. new를 하드코딩하기 전에 질문해보라. 테스트할 때 그 객체를 다른 걸로 교체할 필요가 없을지? 필요가 있다면 메서드 안에서 생성할게 아니라 외부에서 넘겨받 게끔 해야한다. 19 / 32
  • 20.
    생성자에서는로직구현을피하라 테스트에 영향을 미칠만한로직을 생성자에 넣어선 안된다 위 코드는 "ipconfig"를 실행함으로 윈도우즈 컴퓨터에서만 생성할 수 있는 클래 스이다. 다른 운영체제에서도 실행할 수 있게 하려면 생성자의 로직을 메서드로 만들고 오버라이딩 할 수 있게 해주면 어떤 운영체제에서도 해당 객체를 생성하 는 테스트를 만들 수 있다. // 너무 많은 일을 하는 생성자 public class UUID { private String value; public UUID() { long macAddress = 0; Process p = Runtime.getRuntime().exec(new String[] { "ipconfig", "/all" }, BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStre String line = null; while (macAddress == null && (line = in.readLine()) != null) { macAddress = extractMacAddressFrom(line); } ... } } 20 / 32
  • 21.
    싱글톤을피하라 public class Clock{ private static final Clock singleInstance = new Clock(); private Clock() { } public static Clock getInstance() { return singleInstance; } } public class Log { public void log(String message) { String prefix = "[" + Clock.getInstance().timestamp() + "] "; logFile.write(prefix + message); } } 싱글톤은 테스트가 자신에게 필요한 대용품을 만들 수 없게 가로막는다. Clock 객체는 한 번 초기화 되면 절대로 교체할 수 없다. Clock 싱글톤을 사용하는 코드를 검사 할 때마다 상당히 번거롭게 된다.(Clock 객 체를 다른 객체로 변경하고 싶다면 리플렉션이나 별도의 setter가 필요하다) 만약 꼭 정적 싱글톤 메서드를 사용해야겠다면 getInstance() 메서드가 클래스 아닌 인터페이스를 반환하게 하도록 하는걸 추천한다 (???) 21 / 32
  • 22.
    상속보다는컴포지션을사용하라 상속으로 인해 만들어진클래스 계층 구조는 변경할 수 없으므로 테스트 용이성 을 떨어뜨린다. 기능을 재활용하기 위한 목적으로 상속을 이용한다면 컴포지션 방식이 낫다. 컴포지션은 상위 클래스를 호출할 일도 일절 없고, 상속의 상위 클래스의 메 서드 재사용과 달리 다른 구현을 통해 자숑할 수 있고, 실행 도중에도 마음껏 교체할 수 있다. 관련 링크 (자세한 내용은 아래 링크에) Effective Java - 상속 대신 컴포지션 하라 외부라이브러리를감싸라 외부 라이브러리의 경우 코드에 대한 제어권이 우리에게 거의 없다. 테스틀 용이하게 하려면 직접 다른 구현으로 교체하기 쉽고 테스트하기도 편한 인터페이스를 하나 만들어서 그 라이브러리를 감싸자. 22 / 32
  • 23.
    서비스호출을피하라 public class Assets{ public Collection<Asset> search(String... keywords) { APIRequest searchRequest = createSearchRequestFrom(keywords); APICredentials credentials = Configuration.getCredentials(); // setInstance 메서드를 만들면 객체를 바꾸는게 가능하긴 하다 APIClient api = APIClient.getInstance(credentials); return api.search(searchReqeust); } ... } 서비스 호출 방식은 생성자 입력 방식보다 스텁으로 대체하기 훨씬 어렵다. 23 / 32
  • 24.
    해당 코드로 테스트를코드를 만들게 된다면 @Test public void searchingByKeywords() { final String[] keywords = { "one", "two", "three" }; final Collection<Asset> results = createListOfRandomAssets(); APIClient.setInstance(new FakeAPIClient(keywords, results)); Assets assets = new Assets(); assetEquals(results, assets.search(keywords)); } 이 경우 생성자 입력 방식을 쓰는게 오히려 다른 객체로 대체하기 쉽고 명시적이 기 때문에 협력 객체와의 관계가 더 직관적이다. (종속 객체를 직접 전달하라) public class Assets { private APIClient api; public Assets(APIClient api) { this.api = api; } ... } 24 / 32
  • 25.
    제2의JVM 언어를활용한테스트작성 제 2의JVM 언어를 사용하면 자바보다 더 간결하고 표현력이 좋다. 일반적인이점 자질구레한 문법이 적어 핵심 로직이 잘 드러남 더 읽기 쉬운 데이터 구조를 제공 표준 데이터 타입을 편하게 다룰 수 있는 메서드를 추가로 제공 더 강력한 기능을 언어 차원에서 지원 25 / 32
  • 26.
    테스트작성 성능보다 가독성 테스트 코드관점에선 성능은 치명적인 걸림돌이 아니다, 가독성이 더 중요 하다 제품 개발은 계속 자바로, 테스트 코드에만 제2의 JVM 언어를 사용 한정된 영역, 그것도 테스트에만 사용하면 되기에 새로운 언어를 익히고 적 용하는 부담도 크지 않다 테스트용 언어는 따로 있다 대부분의 언어는 애플리케이션이나 시스템 소프트웨어를 개발할 목적으로 설계된다(이는 테스트 작성엔 유용하지 않을 수 있다) 테스트 작성에는 대체로 간결한 문법과 다목적 데이터 구조를 제공하는 언어가 적합하다 가장 적합한 언어는 루비와 그루비다. 26 / 32
  • 27.
    BDD 도구의뛰어난표현력 테스트의 의도를더 명확하게 표현하기 위한 용어를 찾는 과정에서 탄생 이들 도구는 기대하는 동작과 테스트의 의도를 더 명확하게 드러내고 동시에 산만한 문법을 숨겨준다 BDD에 대한 간략한 설명 BDD는 시나리오를 기반으로 테스트 케이스를 작성하며, 개발자가 아닌 사 람이 봐도 이해할 수 있을 정도의 레벨을 권장한다. 하나의 시나리오는 아래와 같은 구조를 가진다 Feature : 테스트 대상의 기능/책임을 명시한다. Scenario : 테스트 목적에 대한 상황을 설명한다. Given : 시나리오 진행에 필요한 값을 설정한다. When : 시나리오를 진행하는데 필요한 조건을 명시한다. Then : 시나리오를 완료했을 때 보장해야 하는 결과를 명시한다. 코드예제 이 테스트 코드는 사실 정기 휴일, 휴무기간에 대한 테스트를 따로 만들었어야 되 는게 맞다. 27 / 32
  • 28.
    @Title("휴무일 여부 테스트") classStoreDayOffTest extends Specification { @Unroll("#SOMEDAY일자는 휴무일 여부가 #EXPECTED여야 한다") def "isDayOff"() { given: "휴무일 정보 생성" // 휴무일 정보 def storeDayOff = new StoreDayOff( regularDaysOfWeek: REGULAR_DAYS_OF_WEEK, temporaryDays: [] ) // 테스트 코드에 이런 IF는 적합하지 않은거 같다 if (START_DATE) { storeDayOff.temporaryDays = [ new StoreDayOff.TemporaryDay( startDate: LocalDate.parse(START_DATE), endDate: LocalDate.parse(END_DATE) ) ] } when: "휴무일 여부 체크" def result = storeDayOff.isDayOff(LocalDate.parse(SOMEDAY)) then: "검증" EXPECTED == result where: "데이터 테이블" REGULAR_DAYS_OF_WEEK | START_DATE | END_DATE | SOMEDAY | EXPECTED [ DayOfWeek.MONDAY ] | "" | "" | "2018-07-23" | true [ DayOfWeek.MONDAY ] | "" | "" | "2018-07-24" | false [] | "2018-07-01" | "2018-07-30" | "2018-07-23" | true [] | "2018-07-01" | "2018-07-30" | "2018-08-01" | false } } 28 / 32
  • 29.
  • 30.
    왜그루비로테스트를작성하는것이좋을까? 그루비 홈페이지에 설명된장점중 일부 내용 makes modern programming features available to Java developers with almost-zero learning curve compiles straight to Java bytecode so you can use it anywhere you can use Java 아래와 같은 링크에 나온 내용 처럼 간결한 문법을 이용한다면 표현력이 좋아지고(가 독성 향상) 테스트 코드를 만드는 생산성이 향상될 수 있다. Groovy - Style Guide Groovy - Differences with Java Groovy - Working with a relational database 30 / 32
  • 31.
    Spock Spock의 기능 중일부 소개하고 싶은 내용 Extension @Ignore 메서드나 클래스 단위 테스트 무시 @IgnoreRest 다른 테스트 메서드는 다 무시하고 이를 설정한 해당 테스트만 실행 됨 @IgnoreIf 조건에 해당하는 경우 테스트 무시 (예:테스트 환경이 윈도우인 경 우) @Requires 조건에 해당하는 경우 테스트 실행 (@IgnoreIf의 반대) @PendingFeature 테스트를 실행하지만 실패시 무시한다 (개발중인 기능) @Stepwise 순차적으로 테스트 메서드를 실행한다. @Timeout 테스트 실행시간이 해당 시간이 지난 경우 테스트 실패로 간주 JUnit 과의 비교 데이터 테이블 데이터 테이블 사용시 시각적으로 기대 결과값을 표현하기 위해 '||'로 구분 31 / 32
  • 32.
    Spock 관련 일부소개하고 싶은 내용 링크 Spock - Comparison to Junit Spock - Data Tables Spock - Unroll Spock - Syntatic Variations Spock - Extensions 32 / 32