어떠한 객체 A가 B라는 객체에 의존적인 경우, 우리는 테스트 코드를 어떻게 작성해야할까?
보통 서로 의존적이지 않은 경우, A를 테스트 할 때, A에 대한 테스트코드를 작성하고, B를 테스트 할 때, B에 대한 테스트만 작성하면 된다.
하지만 그렇지 않은 경우가 있다.
한 객체가 다른 객체에 의존하는 경우가 그렇다.
A객체가 B객체에 의존적일 때, A를 테스트 하기 위해서는 B와 A 객체를 둘을 한번에 테스트 해야만 할것이다. 그것은 결국 A 와 B를 각각 테스트 해야하는, TDD에 위배가 된다 생각 할 수 있다. 그렇다면 어떻게 해야 할까?
→ 이때 나온 개념이 바로 단위 테스트 와 임시 객체이다.
기존에는 A 객체가 B에 의존적일 때, 테스트를 하기 위해서는 A와 B 전체를 테스트 해야 했다. 하지만 '단위' 즉, A객체만 테스트를 하면된다. 그렇다면 B는 어떻게 할까? B는 임시객체로 사용 하면 된다.
요약하자면 A객체가 B에 의존적 일 때, A를 테스트 하기 위해선 B를 임시객체로 만들고, A를 테스트하면 된다.
이해가 안된다면, 코드를 쳐보자.
//A.java
public class A {
private int age;
public A(int age) {
this.age = age;
}
public boolean isOlder(int age){
return this.age<age;
}
}
A.java 설명
isOlder 메소드를 이용해, 객체가 갖고 있는
age와 비교하여 값이 크면 true을 반환하고 그렇지 않으면 false 반환.
//B.java
public class B {
private A a;
private int age = 17;
public B(A a) {
this.a = a;
}
public int isOlderThanA() {
if (a.isOlder(age)) {
return 1;
}
return 0;
}
}
위의 두 자바 코드의 의존성은 단 방향적이다.
B는 A 클래스 타입의 필드를 갖고 있기 때문에 , B에서는 A에 대해 알 수 있는, '연관 관계'적이다.
→ 의존성이 있다는 것을 알 수 있다.
이제 테스트 코드를 살펴보자.
//ATest
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.assertThat;
class ATest {
private A a;
@BeforeEach
public void setUp() {
a = new A(13);
}
@ParameterizedTest
@DisplayName("isOlder을 실행했을 때, 객체의 age 보다 큰 나이가 들어가면 true 반환, 그렇지 않으면 false 반환 ")
@CsvSource(value = {"15,true", "1,false", "10,false"})
void isOrder(int age, boolean expect) {
assertThat(a.isOlder(age)).isEqualTo(expect);
}
}
위 테스트 코드에서
isOrder 테스트 코드는 value에 해당하는 값들이 제대로 전잘되어 값 비교가 되는지 확인 하는 코드이다. 코드 설명은 @DisplayName를 확인해 보면된다.
또한 @ParameterizedTest 와 @CsvSource에 대해서는 나중에 따로 다루겠다.
그냥 쉽게 생각해서 나이를 넣었을 때, 13(@SetUp에서 생성자에 13을 넣어줬기 때문에, age = 13) 을 기준으로 잘 작동하는지 테스트 하는 코드이다.
//BTest
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*;
class BTest {
private A a;
private B b;
@BeforeEach
void setUp() {
a = mock(A.class);
b = new B(a);
}
@Test
@DisplayName("isOlderThanA a의 age를 비교하여 값이 true면 1반환")
void isOlderThanA_1() {
when(a.isOlder(anyInt())).thenReturn(true);
assertThat(b.isOlderThanA()).isEqualTo(1);
}
@Test
@DisplayName("isOlderThanA a의 age를 비교하여 값이 false면 0반환")
void isOlderThanA_0() {
when(a.isOlder(anyInt())).thenReturn(false);
assertThat(b.isOlderThanA()).isEqualTo(0);
}
@ParameterizedTest
@DisplayName("isOlderThanA를 실행했을 때, 객체의 age 보다 큰 나이가 들어가면 1 반환, 그렇지 않으면 0 반환")
@CsvSource(value = {"true,1", "false,0"})
void isOlderThanA_1_and_0(boolean result, int expect) {
when(a.isOlder(anyInt())).thenReturn(result);
assertThat(b.isOlderThanA()).isEqualTo(expect);
}
@Test
@DisplayName("a.check 메서드가 3번 실행 되어야 한다.")
void check() {
b.check();
verify(a, times(3)).check();
}
}
위의 테스트 코드가 사실상 우리가 다뤄야하는 핵심 이다.
코드가 많이 나와 살짝 방황 했을 수 있지만, 다시 처음으로 돌아와서 우리가 알아봐야할 것은, 의존 관계가 있는 경우 그 객체를 어떻게 테스트 하느냐 이다.
지금 B 는 A를 의존 하고 있는 상태이다. 이런 경우 단위테스트를 어떻게 하는지 살펴보자. 우선 가독성을 높히기 위해 위 코드의 일부분을 가져와 보겠다.
import static org.mockito.Mockito.*;
...
class BTest {
...
@BeforeEach
void setUp() {
a = mock(A.class);
b = new B(a);
}
@Test
@DisplayName("isOlderThanA a의 age를 비교하여 값이 true면 1반환")
void isOlderThanA_1() {
when(a.isOlder(anyInt())).thenReturn(true);
assertThat(b.isOlderThanA()).isEqualTo(1);
}
...
}
위 코드에서 주의 깊게 봐야할 것은 setUp 부분 이다
지금 setUP() 을 보면 ' mock ' 을 사용한 것을 알 수 있다. B객체를 생성하기 위해서는 A 객체가 필요하다. 하지만 제대로 된 단위 테스트를 하기위해, 여기서는 A객체를 '임시' 로 생성해줬다.
a = mock(A.class) // a에 A클래스의 임시객체 생성
mock을 사용하기 위해서는 Mockito라는 라이브러리를 임포트 해줘야 한다.
이제 위에서 생략했던 @ParameterizedTest 와 @CsvSource 에 대해 알아보자
하나의 테스트 메소드로 여러 개의 파라미터에 대해서 테스트할 수 있다.
쉽게 말해 여러번 @Test를 짜는 것을 한번에 할 수 있음.
// A.java
public class A {
private int age;
public A(int age) {
this.age = age;
}
public boolean isOlder(int age){
return this.age<age;
}
}
//ATest.java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.assertThat;
class ATest {
private A a;
@BeforeEach
public void setUp() {
a = new A(13);
}
@Test
@DisplayName("isOlder을 실행했을 때, 객체의 age 보다 크면 true 반환")
void isOlder_23() {
assertThat(a.isOlder(23)).isTrue();
}
@Test
@DisplayName("isOlder을 실행했을 때, 객체의 age 보다 작으면 false 반환")
void isOlder_10() {
assertThat(a.isOlder(10)).isFalse();
}
@Test
@DisplayName("isOlder을 실행했을 때, 객체의 age 보다 작으면 false 반환")
void isOlder_3() {
assertThat(a.isOlder(3)).isFalse();
}
}
위 코드를 아래와 같이 줄 일 수 있다.
@ParameterizedTest
@DisplayName("isOlder을 실행했을 때, 객체의 age 보다 큰 나이가 들어가면 true 반환, 그렇지 않으면 false 반환 ")
@CsvSource(value = {"23,true", "10,false", "3,false"})
void isOrder(int age, boolean expect) {
assertThat(a.isOlder(age)).isEqualTo(expect);
}
이제 CsvSoure에 대해 알아보자.
//BTest코드에서 발췌
@CsvSource(value = {"23,true", "10,false", "3,false"})
void isOrder(int age, boolean expect) {
assertThat(a.isOlder(age)).isEqualTo(expect);
}
CsvSource의 value 속성으로 다음과 같이 파라미터를 던질 수 있다.
즉 "23,true", "10,false", "3,false"라는 값들을 순서대로 age, expect로 isOrder 함수의 파라 미터로 보낼 수가 있다.
한번의 여러 테스트를 하는 @ParameterizedTest와 한번의 여러 value를 파라미터로 보낼수 있는
@CsvSoure는 같이 자주 쓰이니 잘 알아두자 ^_^