[실용주의 단위 테스트] 11장_테스트 리팩토링

프로덕션 시스템을 리팩토링하는 것 처럼, 테스트를 리팩토링해야한다.

1. 이해 검색

이해하기 힘든 다음 테스트 코드를 리팩토링 할 것이다. Test Smell 을 찾으며 리팩토링 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SearchTest {
@Test
public void testSearch() {
try {
String pageContent = "There are certain queer times and occasions "
+ "in this strange mixed affair we call life when a man "
+ "takes this whole universe for a vast practical joke, "
+ "though the wit thereof he but dimly discerns, and more "
+ "than suspects that the joke is at nobody's expense but "
+ "his own.";
byte[] bytes = pageContent.getBytes();
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
// search
Search search = new Search(stream, "practical joke", "1");
Search.LOGGER.setLevel(Level.OFF);
search.setSurroundingCharacterCount(10);
search.execute();
assertFalse(search.errored());
List<Match> matches = search.getMatches();
assertThat(matches, is(notNullValue()));
assertTrue(matches.size() >= 1);
Match match = matches.get(0);
assertThat(match.searchString, equalTo("practical joke"));
assertThat(match.surroundingContext,
equalTo("or a vast practical joke, though t"));
stream.close();

// negative
URLConnection connection =
new URL("http://bit.ly/15sYPA7").openConnection();
InputStream inputStream = connection.getInputStream();
search = new Search(inputStream, "smelt", "http://bit.ly/15sYPA7");
search.execute();
assertThat(search.getMatches().size(), equalTo(0));
stream.close();
} catch (Exception e) {
e.printStackTrace();
fail("exception thrown in test" + e.getMessage());
}
}
}

2. 테스트 냄새 : 불필요한 테스트 코드

테스트 메서드에서 try/catch 의 가치는 없다. try/catch 블록을 제거하고 IOException 을 던지도록 변경하자.

1
2
3
4
5
public class SearchTest {
@Test
public void testSearch() throws IOException {
String pageContent =
...

그리고, 아래 코드에서 not-null 단언이 필요할까 ?

1
2
3
List<Match> matches = search.getMatches();
assertThat(matches, is(notNullValue()));
assertTrue(matches.size() >= 1);

matches.size() 에서 matches 가 null 을 참조한다면 예외를 던질 것이다.
따라서 다음과 같이 수정하자.

Read more

[실용주의 단위 테스트] 10장_Mock

Mock 객체를 사용해서 의존성을 끊는 방법을 정리한다.

1. 테스트 도전 과제

다음 클래스의 메서드는, 좌표를 기반으로 Address 객체를 생성해서 반환한다. retrieve() 메서드의 테스트를 작성해보자.
주의할점은, HttpImpl 클래스가 REST 호출을 실행한다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AddressRetriever {

public Address retrieve(double latitude, double longitude)
throws IOException, ParseException {
String parms = String.format("lat=%.6flon=%.6f", latitude, longitude);
String response = new HttpImpl().get(
"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"
+ parms);

JSONObject obj = (JSONObject)new JSONParser().parse(response);

JSONObject address = (JSONObject)obj.get("address");
String country = (String)address.get("country_code");
if (!country.equals("us"))
throw new UnsupportedOperationException(
"cannot support non-US addresses at this time");

String houseNumber = (String)address.get("house_number");
String road = (String)address.get("road");
String city = (String)address.get("city");
String state = (String)address.get("state");
String zip = (String)address.get("postcode");

return new Address(houseNumber, road, city, state, zip);
}
}

HttpImp 클래스는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HttpImpl implements Http {

public String get(String url) throws IOException {
CloseableHttpClient client = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
CloseableHttpResponse response = client.execute(request);
try {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity);
} finally {
response.close();
}
}
}

public interface Http {
String get(String url) throws IOException;
}

HTTP 호출을 실행하면 다음 두 문제가 있다.

  1. 실제 호출에 대한 테스트는, 다른 빠른 테스트들에 비해 느리다.
  2. HTTP API 가 항상 가용한지는 보장할 수 없다. 즉, 우리의 통제 밖이다.

2. 번거로운 동작을 Stub 으로 대체

HTTP 호출에서 반환되는 JSON 응답을 이용해서 Address 객체를 생성하는 로직을 검증해보자.
그러기 위해, HttpImpl 클래스의 get() 메서드 동작을 변경해보자. 단지, 테스트를 위해 하드 코딩한 JSON 문자열을 반환하도록 하자.
이렇게, 테스트 용도로 하드 코딩한 값을 반환하는 구현체를 Stub 이라고 한다.

Read more

[실용주의 단위 테스트] 7장_경계 조건

경계 조건과 관련된 결함을 방지할 수 있는 방법들을 정리한다. 약어로, CORRECT.

1. Conformance

기대하는 구조에 입력 데이터가 맞는지 확인해라.
예를 들면, 이메일 주소의 경우에 @ 기호가 없는 경우나 @ 의 앞부분이 비어있는 경우에 대처해야한다.

2. Ordering

데이터의 순서나 Collection 에 있는 데이터의 위치가 코드를 잘못되게 할 수 있다.

3. Range

기본형의 과도한 사용에 의한 code smell 을 Primitive Obsession 이라고 한다. 객체 지향 언어를 사용하면, 사용자 정의 추상화로 이 문제를 해결할 수 있다.
예를 들면, 원은 360 도 이다. 이동 방향을 자바 기본형으로 저장하기 보다 Bearing 클래스로 범위를 제약하는 로직을 캡슐화 할 수 있다.
다음 테스트 코드는, 유효하지 않은 값으로 Bearing 을 생성할 때 어떤 일이 발생하는지 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class BearingTest {

@Test(expected=BearingOutOfRangeException.class)
public void throwsOnNegativeNumber() {
new Bearing(-1);
}

@Test(expected=BearingOutOfRangeException.class)
public void throwsWhenBearingTooLarge() {
new Bearing(Bearing.MAX + 1);
}

@Test
public void answersValidBearing() {
assertThat(new Bearing(Bearing.MAX).value(), equalTo(Bearing.MAX));
}

@Test
public void answersAngleBetweenItAndAnotherBearing() {
assertThat(new Bearing(15).angleBetween(new Bearing(12)), equalTo(3));
}

@Test
public void angleBetweenIsNegativeWhenThisBearingSmaller() {
assertThat(new Bearing(12).angleBetween(new Bearing(15)), equalTo(-3));
}
}

제약 사항은 생성자에 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Bearing {
public static final int MAX = 359;
private int value;

public Bearing(int value) {
if (value < 0 || value > MAX) throw new BearingOutOfRangeException();
this.value = value;
}

public int value() { return value; }

public int angleBetween(Bearing bearing) { return value - bearing.value; }
}
Read more

[실용주의 단위 테스트] 6장_무엇을 테스트할 것인가?

무엇을 테스트해야하는지 정리한다.
이것으로 쉽게 요약할 수 있다 : Right-BICEP

  1. Right : 결과가 올바른가 ?
  2. B : Boundary. 경계 조건은 맞는가 ?
  3. I : Inverse relationship. 역관계를 검사할 수 있나 ?
  4. C : Cross check. 다른 수단을 활용해서 교차 검사 가능한가 ?
  5. E : Error. 오류를 강제로 발생시킬 수 있는가 ?
  6. P : Performance. 성능 조건은 기준에 부합하나 ?

1. Right

테스트 코드는 무엇보다도 먼저, 기대한 결과를 산출하는지 검증 해야한다.
다음 코드의 ScoreCollection 에 더 많은 숫자나 더 큰 수를 넣어서 테스트를 강화할 수 있다. 하지만 이러한 테스트는 행복 경로 테스트의 영역일 뿐이다.

1
2
3
4
5
6
7
8
9
10
11
12
public class ScoreCollectionTest {
@Test
public void answersArithmeticMeanOfTwoNumbers() {
ScoreCollection collection = new ScoreCollection();
collection.add(() -> 5);
collection.add(() -> 7);

int actualResult = collection.arithmeticMean();

assertThat(actualResult, equalTo(6));
}
}

2. Boundary

대부분의 결함은 Corner Case 이다. 테스트로 이것들을 처리해야한다. 다음과 같은 경계 조건이 있다.

  1. 모호하고 일관성 없는 입력 값. ex) 특수문자가 포함된 파일 이름
  2. 잘못된 양식의 데이터
  3. Overflow 를 일으키는 계산
  4. 비거나 빠진 값. ex) 0, “”, null
  5. 이성적인 기대 값을 벗어나는 값. ex) 200 세의 나이
  6. 중복을 허용해서는 안되는 목록에 중복 값이 있는 경우
  7. 정렬이 안된 정렬 리스트 혹은 그 반대
  8. 시간 순이 맞지 않는 경우. ex) HTTP Server 가 OPTIONS 메서드의 결과를 POST 메서드 보다 나중에 반환

다음 클래스를 기준으로 테스트해보자.

1
2
3
4
5
6
7
8
9
10
11
12
public class ScoreCollection {
private List<Scoreable> scores = new ArrayList<>();

public void add(Scoreable scoreable) {
scores.add(scoreable);
}

public int arithmeticMean() {
int total = scores.stream().mapToInt(Scoreable::getScore).sum();
return total / scores.size();
}
}
Read more

[실용주의 단위 테스트] 5장_좋은 테스트의 FIRST 속성

의미있는 테스트를 만드는데 도움을 주는 핵심 개념을 정리한다.
좋은 테스트 조건 : FIRST ( Fast / Isolated / Repeatable / Self-validating / Timely )

1. Fast

테스트를 빠르게 유지해라. 단위 테스트를 하루에 서너 번 실행하기도 버겁다면 잘못된 것이다. 예를 들면, 모든 테스트 코드가 데이터베이스를 호출하면 전체 테스트 역시 느릴 것이다.

2. Isolated

좋은 단위 테스트는,

  1. 검증하려는 작은 양의 코드에 집중한다. ‘단위’ 라고 말하는 정의와 부합한다.
  2. 다른 단위 테스트에 의존하지 않는다. 만약, 여러 테스트가 값 비싸게 생성된 데이터를 재사용 하는 방식으로 테스트 순서를 조작해서 전체 테스트의 실행 속도를 높이려 할 수도 있다. 하지만, 이것은 의존성의 악순환만 발생시킨다. 테스트 실패시, 무엇이 원인인지 알아내느라 오래 걸릴 수 있다.

객체 지향 설계에서 SRP 는 클래스를 변경해야할 이유가 하나민 있어야한다고 말한다. 테스트 메서드도 마찬가지다. 테스트 메서드가 하나 이상의 이유로 깨지면 테스트를 분할해라.
그리고, 테스트에 두 번째 단언을 추가할 때 다음을 먼저 고민해보자.

이 단언이 단일 동작을 검증하도록 돕나 ?

아니면, 새로운 테스트 이름으로 기술할 수 잇는 어떤 동작인가 ?

3. Repeatble

좋은 테스트는, 실행할 때마다 결과가 같아야한다. 이러기 위해서는, 직접 통제할 수 없는 외부 환경과 격리시켜야한다.

Read more

[실용주의 단위 테스트] 4장_테스트 조직

이번 장은 다음 주제를 다룬다.

  1. 준비-실행-단언을 사용해 테스트를 가시적이고 일관성 있게 만드는 방법
  2. 메서드를 테스트하는 것이 아니라, 동작을 테스트
  3. 테스트 이름의 중요성
  4. @Before, @After

1. AAA 로 테스트 일관성 유지

AAA 는 다음과 같다.

  • Arrange : 준비. 테스트 코드를 실행하기 전에 시스템에 적절한 상태에 있는지 확인
  • Act: 실행. 테스트 코드 실행
  • Assert : 단언. 실행한 코드가 기대한 대로 동작하는지 확인

예를 들면,

1
2
3
4
5
6
7
8
9
10
@Test
public void answerArithmeticMeanOfTwoNumbers() {
ScoreCollection collection = new ScoreCollection();
collection.add(() -> 5);
collection.add(() -> 7);

int actualResult = collection.arithmeticMean();

assertThat(actualResult, equalTo(6));
}

2. 동작 테스트 vs 메서드 테스트

단위 테스트를 작성할 때는, 개별 메서드를 테스트 하는 것이 아니라 클래스의 종합적인 동작을 테스트해야한다.

3. 테스트와 프로덕션 코드의 관계

Read more