Spring : Given-When-Then Pattern
Given-When-Then Pattern
Given When Then은 테스트 코드를 작성할 때, 테스트코드의 라이프사이클을 정해놓은 영역을 의미합니다. 즉, Given-When-Then Pattern은 테스트할 준비를 하고(Given), 테스트할 대상을 실행하고(When), 검증(Then)하는 것입니다. 이 패턴은 BDD(Behavior Driven Development)에서 나온 패턴인데, BDD는 TDD에서 테스트라는 말이 너무 추상적이어서 테스트를 행위로 바꾼 행위 주도 개발법을 말합니다. 아주 간단한 패턴이지만 저 패턴을 잘 지키면 테스트 코드를 이해하기가 쉬워지니 지키도록 노력하는게 좋습니다.
간단하게 SpringMVC에서 Category의 개수를 가져오는 Service를 테스트하는 코드를 보며 설명하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void countTest() throws Exception {
// given
given(categoryDao.selectCount()).willReturn(1);
// when(categoryDao.selectCount()).thenReturn(1);
//when
int result = categoryService.getCount();
//then
verify(categoryDao).selectCount();
assertThat(result, is(1));
// Assert.assertEquals(1, result);
}
Given
테스트를 위해 준비하는 과정 입니다. 테스트에 사용하는 변수, 입력 값 등을 정의하거나 Mock 객체를 정의하는 구문도 Given에 포함합니다.
1
2
3
// given
given(categoryDao.selectCount()).willReturn(1);
// when(categoryDao.selectCount()).thenReturn(1);
위의 코드는 categoryService에서 사용하는 categoryDao의 selectCount() 메서드의 결과를 1로 정의합니다.
Mockito.when
1
2
3
4
5
6
7
import static org.mockito.Mockito.when;
@Test
public void countTest() throws Exception {
when(categoryDao.selectCount()).thenReturn(1);
...
}
Mockito의 when() 메서드는 categoryDao의 selectCount() 메서드의 결과를 정의하는 것 이기 때문에 Given에 포함됩니다. 하지만 Given의 과정에 when이 나오는 것은 헷갈릴 수 있는 가능성이 있습니다.
BDDMockito.given
1
2
3
4
5
6
7
import static org.mockito.BDDMockito.given;
@Test
public void countTest() throws Exception {
given(categoryDao.selectCount()).willReturn(1);
...
}
Mockito.when을 사용할 때, Given의 과정에 when이 나오는 것은 헷갈릴 수 있는 가능성이 있는데, 이 문제를 해결하기 위해 등장한 것이 BDDMockito 입니다. BDDMockito는 Mockito를 상속한 클래스이고, 동작이나 사용하는 방법 또한 Mockito와 거의 차이가 없습니다. 즉, BDDMockito는 BDD를 사용하여 테스트코드를 작성할 때, 시나리오에 맞게 테스트 코드가 읽힐 수 있도록 도와주는 (이름을 변경한) 클래스 입니다.
When
실제로 테스트할 메서드를 실행하는 과정입니다. 하나의 메서드만 수행하는 것이 바람직하기 때문에, 일반적으로 When의 과정은 테스트 코드에서 가장 심플하고도 중요한 구문입니다.
1
2
//when
int result = categoryService.getCount();
위의 코드는 categoryService의 getCount()메서드를 실행합니다.
Then
테스트를 검증하는 과정입니다. 예상한 값, 실제 실행을 통해서 나온 값을 검증합니다.
1
2
3
4
//then
verify(categoryDao).selectCount();
assertThat(result, is(1));
// Assert.assertEquals(1, result);
위의 코드는 verify() 메서드로 selectCount()가 실행된 적이 있는지 검증하고, result의 값에 기대하는 값이 나왔는지 확인합니다.
util.Assert.assertEquals()
1
2
3
4
5
6
7
8
import org.junit.Assert;
@Test
public void countTest() throws Exception {
...
Assert.assertEquals(1, result);
...
}
기대하는 값이 나오는지 확인하는 assertEquals() 메서드는 첫 번째 인자로 기대값을 받고, 두 번째 인자로 실제값을 받습니다. 실제값이 앞에 들어가는게 자연스러워보이기 때문에 인자의 위치에 대해서 헷갈릴 수 있는 메서드입니다.
junit.Assert.assertThat()
1
2
3
4
5
6
7
8
9
10
11
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import org.junit.Assert;
@Test
public void countTest() throws Exception {
...
assertThat(result, is(1));
...
}
assertThat()은 hamcrest가 static 메서드로 제공하는 여러 matcher를 사용할 수 있고 이러한 static 메서드는 체이닝할 수 있어서 기존 assertXXX 메서드보다 더 많은 유연성을 제공합니다. 그 외에도 다양한 이점이 있습니다.
가독성
assertEquals()를 사용할 때 마다 인자의 위치에 대해서 헷갈릴 때가 많기 때문에 assertThat()을 사용해서 작성하면 그 의미를 더 분명히 할 수 있습니다. assertThat()의 첫 번째 인자는 실제값을 받고, 두 번째 인자는 기대값을 받습니다. 이 때, 두 번째 값에는 단순히 값만 들어가는 것이 아니고, hamcrest가 static 메서드로 제공하는 matcher가 들어가게 됩니다. (import static org.hamcrest.CoreMatchers.is)
Failure 메세지
assertThat()은 더 나은 에러 메시지를 제공합니다.
assertTrue()를 사용하는 예
1
2
3
4
5
6
7
assertTrue(expected.contains(actual));
--- 실행 결과 ---
java.lang.AssertionError
at org.junit.Assert.fail(Assert.java:86)
at org.junit.Assert.assertTrue(Assert.java:41)
at org.junit.Assert.assertTrue(Assert.java:52)
특정 string을 포함하는지 확인하기 위해서는 assertStringContains()와 같은 메서드가 없기 때문에 contains() 메서드를 사용하고 assertTrue()을 이용해 검증해야 합니다. 여기서 assertion error는 expected 값과 actual 값에 대해 알려주지 않는다는 것입니다.
assertThat을 사용하는 예
1
2
3
4
5
6
7
8
9
assertThat(actual, containsString(expected));
--- 실행 결과 ---
Expected: a string containing "expected"
but: was "actual"
java.lang.AssertionError:
Expected: a string containing "expected"
but: was "actual"
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
expected 값과 actual 값 모두 에러 메시지에 반환됩니다. 원인을 찾기 위해 별도의 디버깅 필요없이 에러 메시지 만으로 잘못된 부분을 바로 파악할 수 있습니다.
Type 안정성
assertThat을 사용하면 Type에 대한 안정성도 얻을 수 있습니다.
assertEquals()를 사용하는 예
1
2
3
static public void assertEquals(Object expected, Object actual) {
assertEquals(null, expected, actual);
}
assertEquals()의 구현은 위와 같이 Object를 인자로 받고 있기 때문에 아래의 코드에서 컴파일에는 성공하지만, 실행시 실패합니다.
1
assertEquals("abc", 123); // 실행시 실패
assertThat()를 사용하는 예
1
2
3
public static <T> void assertThat(T actual, Matcher<? super T> matcher) {
assertThat("", actual, matcher);
}
assertThat()의 구현은 위와 같이 Generic을 사용하고 있어 Type이 체크되기 때문에,
같은 경우에 assertThat()은 assertEquals()와 다르게 컴파일을 허용하지 않습니다.
1
assertThat(123, is("abc")); // 컴파일 실패
유연성
hamcrest는 anyOf()와 allOf()와 같은 논리 matcher도 제공합니다.
1
assertThat("test", allOf(is("test2"), containsString("te")));
allOf() matcher는 and 논리 연산자처럼 동작합니다. 따라서 제공된 모든 matcher에 통과해야 합니다. 실패하는 경우 다음과 같이 실패한 matcher에 대해서 에러 메시지를 반환합니다.
1
2
3
4
5
6
Expected: (a string containing "te" and is "test2")
but: is "test2" was "test"
java.lang.AssertionError:
Expected: (a string containing "te" and is "test2")
but: is "test2" was "test"
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
Custom Matchers
assertThat()에서 다양한 matcher 이용해 custom assert 메서드를 만들 수 있는 것처럼 다음과 같이 custom matcher를 생성할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
public static class CustomMatcher extends TypeSafeMatcher<String> {
private final String expected;
public CustomMatcher(String expected) {
this.expected = expected;
}
@Override
protected boolean matchesSafely(String item) {
return expected.equals(item);
}
@Override
public void describeTo(Description description) {
description.appendValue(expected);
}
}
Reference
assert 메서드와 그에 상응하는 assertThat(hamcrest 1.3 기준) matcher에 대해서 알아봅니다.
| assert method | assertThat | static Import |
|---|---|---|
| assertEquals(“expected”, “actual”); | assertThat(“actual”, is(“expected”)); | org.hamcrest.core.Is.is |
| assertArrayEquals(new String[]{“test1”, “test2”}, new String[]{“test3”, “test4”}); | assertThat(new String[]{“test1”, “test2”}, is(new String[]{“test3”, “test4”})); | org.hamcrest.core.Is.is |
| assertTrue(value); | assertThat(actual, is(true)); | org.hamcrest.core.Is.is |
| assertFalse(value); | assertThat(actual, is(false)); | org.hamcrest.core.Is.is |
| assertNull(value); | assertThat(actual, nullValue); | org.hamcrest.core.IsNull.nullValue |
| assertNotNull(value); | assertThat(actual, notNullValue); | org.hamcrest.core.IsNull.notNullValue |
| assertSame(expected, actual); | assertThat(actual, sameInstance(expected)); | org.hamcrest.core.IsSame.sameInstance |
| assertNotSame(expected, actual); | assertThat(actual, not(sameInstance(expected))); | org.hamcrest.core.IsNot.not, org.hamcrest.core.IsSame.sameInstance |
| assertTrue(1 > 3); | assertThat(1, greaterThan(3)); | org.hamcrest.number.OrderingComparison.greaterThan |
| assertTrue(“abc”.contains(“d”)); | assertThat(“abc”, containsString(“d”)); | org.hamcrest.core.StringContains.containsString |
이 외에도 더 많은 matcher가 있습니다.
- org.hamcrest.beans
- HasProperty
- HasPropertyWithValue
- SamePropertyValuesAs
- org.hamcrest.collection
- IsArray
- IsArrayContaining
- IsArrayContainingInAnyOrder
- IsArrayContainingInOrder
- IsArrayContainingWithSize
- IsCollectionWithSize
- IsEmptyCollection
- IsEmptyIterable
- IsIn
- IsIterableContainingInAnyOrder
- IsIterableContainingInOrder
- IsIterableWithSize
- IsMapContaining
- org.hamcrest.number
- BigDecimalCloseTo
- IsCloseTo
- OrderingComparison
- org.hamcrest.object
- HasToString
- IsCompatibleType
- IsEventFrom
- org.hamcrest.text
- IsEmptyString
- IsEqualIgnoringCase
- IsEqualIgnoringWhiteSpace
- StringContainsInOrder
참고 : https://brunch.co.kr/@springboot/292
https://jongmin92.github.io/2020/03/31/Java/use-assertthat/