[실용주의 단위 테스트] 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 을 참조한다면 예외를 던질 것이다.
따라서 다음과 같이 수정하자.

1
2
List<Match> matches = search.getMatches();
assertTrue(matches.size() >= 1);

3. 테스트 냄새 : 추상화 누락

잘 구성된 테스트 시스템은 다음 세 가지 관점으로 구성된다.

  1. 데이터 준비
  2. 시스템과 동작
  3. 결과 단언

각 단계를 위해 자세한 코드가 필요할 수 있지만, 세부 사항을 추상화하여 이해하기 쉽게 만들 수 있다.
아래 코드는, 매칭된 목록이 특정 검색 문자열과 주변 맥락을 포함하는 단일 항목을 포함하고 있는지 테스트한다.

1
2
3
4
5
List<Match> matches = search.getMatches();
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"));

이는, 사용자 정의 단언문으로 수정할 수 있다.
아래 코드의, containsMatches 는 ContainMatchers 사용자 정의 클래스의 static method 이다. (코드는 생략)

1
2
3
4
5
6
7
8
9
public class SearchTest {
@Test
public void testSearch() throws IOException {
...
assertThat(search.getMatches(), containsMatches(new Match[] {
new Match("1", "practical joke",
"or a vast practical joke, though t") }));
}
}

또, 다음 코드는 결과 크기가 0 인지 단언하는 코드이다.

1
assertThat(search.getMatches().size(), equalTo(0));

다음과 같이 변경할 수 있다.

1
assertTrue(search.getMatches().isEmpty());

4. 테스트 냄새 : 부적절한 정보

다음 코드들은 불분명한 매릭 리터럴 (상수로 선언되지 않은 숫자 리터럴) 를 포함하고 있다.
문자열 “1” 이 무엇을 의미하는지 확신할 수 없다.

1
Search search = new Search(stream, "practical joke", "1");
1
2
3
assertThat(search.getMatches(), containsMatches(new Match[] { 
new Match("1", "practical joke",
"or a vast practical joke, though t") }));

다음처럼, 의미 있는 이름을 가진 상수를 도입해서 의미를 바로 파악할 수 있도록 하자.

1
2
3
4
5
6
7
8
9
public class SearchTest {
private static final String A_TITLE = "1";
@Test
public void testSearch() throws IOException {
...
Search search = new Search(stream, "practical joke", A_TITLE);
...
}
}

5. 테스트 냄새 : 부푼 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SearchTest {
private static final String A_TITLE = "1";
@Test
public void testSearch() throws IOException {
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);
...

위 코드는 다음과 같이 도우미 메서드를 통해, 세부 사항을 숨길 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SearchTest {
private static final String A_TITLE = "1";

@Test
public void testSearch() throws IOException {
InputStream stream =
streamOn("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.");
...
}

private InputStream streamOn(String pageContent) {
return new ByteArrayInputStream(pageContent.getBytes());
}
}

6. 테스트 냄새 : 다수의 단언

여러 개의 단언이 있다는 것은 테스트 케이스를 여러 개 포함하고 있다는 증거이다.
테스트를 분할해라.
테스트마다 단언이 한개이면 테스트 메서드명을 깔끔하게 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SearchTest {
private static final String A_TITLE = "1";

@Test
public void returnsMatchesShowingContextWhenSearchStringInContent()
throws IOException {
...
}

@Test
public void noMatchesReturnedWhenSearchStringNotInContent()
throws MalformedURLException, IOException {
...
}
}

7. 테스트 냄새 : 테스트와 무관한 세부 사항들

군더더기들은 @Before 와 @After 메서드로 이동해라.

테스트를 이해하는데 필요한 유용한 정보는 옮기지 말아라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SearchTest {
private static final String A_TITLE = "1";
private InputStream stream;

@Before
public void turnOffLogging() {
Search.LOGGER.setLevel(Level.OFF);
}

@After
public void closeResources() throws IOException {
stream.close();
}
}

자바와 JUnit 을 활용한 실용주의 단위 테스트 <제프 랭어, 앤디 헌트, 데이브 토마스>

Comments