티스토리 뷰

Java

Test#01. jUnit으로 테스트하기

LichKing 2017. 8. 18. 17:08

1. jUnit Test

자바로 테스트코드를 짤때 가장 유명한 프레임워크는 jUnit이다. 먼저 테스트 코드를 한번 만들어보자.


dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
}

jUnit 의존성을 추가해주자(아주 간단한거지만 처음으로 gradle을 사용해본다!).


아주간단한 계산기 클래스를 만들어보자. 가장 처음 만들건 더하기 연산이다. 테스트코드부터 만들어보자.


@Test
public void plus() throws Exception {
// given
Calculator calculator = new Calculator();

// when
int result = calculator.plus(2, 5);

// then
assertEquals(7, result);
}


간단한 코드지만 분석을 해보자.

given, when, then 이라고 주석을 쳐놓은건 테스트코드의 라이프사이클을 정해놓은 영역구분인데 테스트할 준비를 하고(given), 테스트할 대상을 실행하고(when), 검증(then)하는 패턴이다. BDD(Behavior Driven Development)에서 나온 패턴인데 BDD는 TDD에서 테스트라는 말이 너무 추상적이어서 테스트를 행위로 바꾼 개발법이다. 아주 간단한 패턴이지만 저 패턴을 잘 지키면 테스트 코드를 이해하기가 쉬워지니 지키도록 노력해보자.


계산기 클래스의 인스턴스를 생성하고, 연산을 할 숫자들을 인자로 보내어 결과를 얻은 뒤 해당 결과를 assertEquals() 메서드를 이용해 검증하고있다. assertEquals() 메서드는 첫인자로 기대값을 받고, 두번째 인자로 실제값을 받는다. 실제값이 앞에 들어가는게 자연스러워보이는데 쓸때마다 인자 순서가 헷갈리는 메서드이다.


* Clean Code(로버트 C 마틴)에서 해당 메서드의 명칭과 인자 순서에 대해 대차게 까는 내용이 나온다.


요즘은 더욱 가독성을 살린 assertThat() 을 많이 사용한다.


// then
assertThat(result, is(7));


어떤 메서드를 사용하든 검증만하면 되는데 여튼 지금은 검증이고뭐고 컴파일이 안될것이다. 실제 클래스를 만들어보자.


public class Calculator {
public int plus(int target1, int target2){
return target1 + target2;
}
}


테스트할 대상도 없이 먼저 테스트부터 만든게 이해가 가지 않을수도 있겠지만 그게 TDD의 핵심이다. 어떻게 테스트할지를 먼저 생각해봄으로써 좀 더 테스트하기 쉬운 설계를 하게되고, 그것이 곧 좋은 설계로 가게끔 만드는 것이다. TDD는 사실 아주 지루하고 고된 작업이라 습관화하기가 쉽지않은데, 나도 이번 포스팅을 작성하면서 연습을 해보려한다.


현재는 덧셈을 수행할 숫자를 2개만 받고있다. 숫자를 2개 이상 받을 수 있게끔 수정해보자. 물론 테스트부터 작성한다.


@Test
public void plus_multiple() throws Exception {
// given
Calculator calculator = new Calculator();

// when
int result = calculator.plus(2, 5, 7, 10);

// then
assertThat(result, is(24));
}


물론 컴파일 되지 않을것이다. 컴파일이 되게끔해보자. 가변인자를 사용하면 좋을것 같다.


public int plus(int... targets){
return Arrays.stream(targets)
.sum();
}


Stream은 자바8에 추가된 인터페이스이다. 자바8에 대한건 다른 포스팅을 참고하자.

테스트 메서드가 단 2개뿐이지만 Calculator 객체를 생성하는 부분이 중복되고있다. 테스트 코드역시 실제코드와 함께 지속적으로 사용되고, 확인되어야할 코드이므로 리팩토링이 필요하다. 중복 부분을 뽑아내자.


public class CalculatorTest {
private Calculator calculator;

public CalculatorTest(){
this.calculator = new Calculator();
}

@Test
public void plus() throws Exception {
// when
int result = calculator.plus(2, 5);

// then
assertThat(result, is(7));
}

@Test
public void plus_multiple() throws Exception {
// when
int result = calculator.plus(2, 5, 7, 10);

// then
assertThat(result, is(24));
}
}


이런식으로 Calculator 변수를 인스턴스 필드로 추출하고, 생성자에서 객체를 생성하게 했다. 지금 수준의 코드에서는 이런식으로 처리해줘도 테스트가 잘 돌아가지만 Calculator 객체가 상태를 갖고있어서 각 테스트간의 상태를 변경하게되면 테스트 순서에 따라 테스트가 성공할수도, 실패할수도 있는 상황이 되게된다.


객체 생성을 생성자에서하지말고 jUnit이 지원하는 라이프사이클 기능을 이용하자.


private Calculator calculator;

@Before
public void setUp(){
this.calculator = new Calculator();
}


생성자 부분을 이런식으로 수정했다. 메서드 명칭은 크게 상관없으며 핵심은 @Before 애노테이션인데, 해당 애노테이션이 달리게되면 테스트메서드를 수행할때마다 그에 앞서 실행되게 된다. 즉 현재 테스트메서드가 2개이니 테스트보다 먼저 실행되어 총 2번 실행되는것이다. @Before를 사용하면 항상 새로 생성된 객체를 사용하게되니 테스트간의 의존성이 사라지게된다.

하지만 딱 생각해봐도 지금같은상황에서는 오버스펙이다. Calculator 객체는 상태가 없으므로 1번만 생성해도된다. 그냥 다시 생성자로 바꾸는게 성능상 나아보인다.


private static Calculator calculator;

@BeforeClass
public static void setUp(){
calculator = new Calculator();
}


jUnit에서는 1번만 실행되어도 되는 코드에 대한 애노테이션도 지원하고있다. 다만 static 메서드이어야한다. 개인적으론 정말 꼭 1번만 실행되어야하는 코드라거나, 테스트가 느려서 문제가 발생하는게 아니라면 가급적 Before 애노테이션을 사용하는걸 권하고싶다. 괜히 마이크로 최적화 하겠다고 BeforeClass썼다가 의도치않게 테스트가 실패하는 경우를 몇번 겪었다.


* @Before, @BeforeClass 와 대응 되는 애노테이션으로 @After, @AfterClass 가 있다.


2. Mocking

이번엔 사용자가 전달하는 인자외에 난수를 발생시켜 난수까지 더하는 내용을 만들어보자. 내가 예제를 만드는 재주가없어 이해하기 어렵다면 일단 테스트부터 확인해보자.


@Test
public void plus(){
// given
RandomCalculator randomCalculator = new RandomCalculator(new Random(), new Calculator());

// when
int result = randomCalculator.plus(2, 5);

// then
assertThat(result, is(10));
}


RandomCalculator 클래스를 만든적이 없으니 당연히 컴파일에러가 발생할 것이다. 아까랑 거의 비슷한 테스트지만 2, 5를 인자로 던지고 10을 기대하는게 좀 이상하다. 랜덤값으로 3이 나오길 기대한것이다. 이제 클래스를 만들자.


public class RandomCalculator {
private Random random;
private Calculator calculator;

public RandomCalculator(Random random, Calculator calculator){
this.calculator = calculator;
this.random = random;
}

public int plus(int... targets){
return this.calculator.plus(targets) + random.nextInt(4);
}
}


random 값을 구할때 4를 넣어준건 일단 현재 테스트를 성공하기위해 임의로 넣었다. 테스트를 10번돌리면 2~3번정도 성공하고있다. 그럼 이 테스트는 항상 이렇게 random값에 의존적이어야할까? 항상 성공하는 테스트를 만들려면 이런 방법은 어떨까?


@Test
public void plus(){
// given
RandomCalculator randomCalculator = new RandomCalculator(new Random(), new Calculator());

// when
int result = randomCalculator.plus(2, 5);

// then
while(true){
if(result == 10) {
break;
}
}

assertThat(result, is(10));
}


테스트를 돌려보면 알겠지만 성공하긴한다..^^ 하지만 딱봐도 뭔가 문제가 있는 코드라는 생각이 들기도하고 숫자범위가 커진다면 무한반복에 빠질수도있다.


다른방법은 없을까? 테스트할때만큼은 Random이 내가 의도한 값을 주게끔 바꾸는건 어떨까?


@Test
public void plus(){
// given
RandomCalculator randomCalculator = new RandomCalculator(new Random(){
@Override
public int nextInt(){
return 3;
}
}, new Calculator());

// when
int result = randomCalculator.plus(2, 5);

// then
assertThat(result, is(10));
}


Random 클래스를 상속받아 nextInt() 메서드가 무조건 3을 주게끔 오버라이딩했다. 이후 테스트는 10번 실행하면 10번 성공하게된다.


* 오버라이딩 메서드를 잘 보자. 인자가 없는 nextInt() 메서드이다. 고로 RandomCalculator 클래스에 있는 nextInt(4) 에서 4를 지워줘야 한다.


지금은 테스트 메서드가 1개뿐이니 익명 클래스로 상속을 했지만 이후 여러 테스트가 추가된다면 테스트 클래스로 분리하는게 나을것이다. 이렇게 테스트에만 사용되는 설정을 테스트 더블이라고 표현한다. 테스트 더블내에는 Dummy, Fake, Spy, Mock 등 구분되어지는 용어들이 있는데 보통 Mock객체라고 퉁쳐서 표현한다.


이런식으로 직접 Mock객체를 생성해줘도 되지만 Mock을 지원하는 라이브러리들이 있다. 이번 포스팅에서는 Mockito를 사용해보겠다.


dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
testCompile group: 'org.mockito', name: 'mockito-all', version: '1.8.4'
}


Mockito를 받아오자.


@Test
public void plus_mockito(){
// given
Random random = mock(Random.class);
when(random.nextInt()).
thenReturn(3);
RandomCalculator randomCalculator = new RandomCalculator(random, new Calculator());

// when
int result = randomCalculator.plus(2, 5);

// then
assertThat(result, is(10));
}


코드를 살펴보자. 메서드 내의 2~4라인이 뭔가 바뀐거같은데 첫번째 라인은 Mock 객체를 만드는것이다. 현재 코드에서는 Random 클래스가 들어갔지만 인자에는 인터페이스도 들어갈 수 있으며, 인터페이스가 들어가면 Mock 구현체를 반환한다.

그 아래 코드는 메서드의 내용을 조작하는 코드이다. random 객체의 nextInt() 메서드를 호출하면 3을 리턴하도록 설정했다.

그리고 아래 RandomCalculator에 Mockito를 이용해 생성한 random 객체를 전달하고 테스트는 성공하는걸 볼 수있다.


이번 포스팅에서 알아본 테스트는 정말 간단한 수준이라 실제 코드에 적용하려면 어려움이 있을수도있다. 하지만 테스트는 습관이며 한번 습관을 들이고 테스트 케이스의 보호아래 리팩토링하는 즐거움을 느낀다면 TDD까지는 못해도 테스트를 작성하는걸 빼먹기는 아주 힘들어지게된다. 각 라이브러리의 사용법을 검색해가며 테스트를 작성해보고, 그로인한 이로움을 느껴보기바란다.


예제코드는 https://github.com/LichKing-lee/groovy-test-study 에서 확인할 수 있다.

'Java' 카테고리의 다른 글

Utils 클래스 리팩토링  (0) 2017.08.27
Test#02. Groovy로 테스트하기  (0) 2017.08.20
Test#01. jUnit으로 테스트하기  (0) 2017.08.18
DDD#01. Domain Object  (1) 2017.07.23
다형성 연습하기  (0) 2017.07.08
Garbage Collector  (0) 2017.07.05
공유하기 링크
TAG
댓글
댓글쓰기 폼