본문 바로가기

Hadoop

[Hadoop] MRUnit,Mockito를 사용한 테스트 케이스 작성 -Mapper-

하둡 완벽 가이드 책에서는 Mockito와 hamcrest를 사용한 테스트 케이스의 예제와
Tool을 활용하여 Driver의 테스트를 하는 것이 나옵니다. 하지만 Tool을 사용 할 경우 윈도우에서는
cygwin을 설치해야 실제 output을 볼 수가 있는 것 같습니다. 에러 내용을 보면 말이죠...
그런점에서 보면 맥이면 Mockito와 Tool을 사용 한  예제가 더 유용 할 수도 있겠네요..

그리고 책에서 잠깐 언급되는 MRUnit이라는 것이 있는데 이 MRUnit을 사용한 테스트 케이스 예제를 소개하려
합니다.

Mockito를 사용한 예제도 새로운 Map/Reduce API에서 사용되는 Context를 활용하여
작성한 예제를 소개하려고 합니다.

 우선 Mapper, Reducer에 대한 테스트케이스를 메서드로 분리하여
작성해보았습니다. 보시면 두 방법의 차이점에 대해서 한번에 보실 수 있을 거에요..

테스트케이스의 샘플이 되는 예제는
Hadoop example로 들어있는 WordCount에 대한 테스트 케이스입니다.
그럼 우선 WordCountMapper를 보겠습니다.



Mapper에 대한 TestCase 입니다.


Mockito를 사용하기 위해서는 관련 라이브러리가 있어야 하는데 Hadoop을 다운받아 보시면
lib안에 Mockito관련 라이브러리를 받아 보실 수 있습니다.

하둡 완벽 가이드 책에서는 Mockito와 함께 hamcrest를 같이 사용하고 있기 때문에
Assert문이 일반적인 JUnit하고는 조금 다릅니다. 

testWithMockito 메서드를 보시면 mapper를 생성하고
입력의 역할을 할 value Text를 생성합니다.

하둡 완벽 가이드 책에서는 여기서 Collector를 사용하여 테스트케이스를 작성하였지만
최신 API에서는 Context를 사용하므로 이부분을 Context를 사용하여 테스트를 할 수 있도록 하였습니다.
그리고 이 과정에서 org.mockito.Mocktio.mock 메서드를 통해 mock 객체를 생성하여
테스트를 진행합니다.

최종 테스트문은 아래와 같습니다.

verify(context, times(2)).write(new Text("aab"), new IntWritable(1));



aab가 2번 output으로 작성되었다.. 정도로 보시면 될 것 같습니다.
Mapper의 일이 Tokenizer로 잘라서 Context로 넘기는 역할이다 보니
위와 같은 검증을 하는 것 같습니다. (aab가 두번 나오죠?)

실패 할 경우 아래와 같은 메시지가 나옵니다.
아래 메시지는 위 verify문을 아래처럼 고쳤을 때의 메시지입니다.

verify(context, times(3)).write(new Text("aab"), new IntWritable(1));



org.mockito.exceptions.verification.TooLittleActualInvocations: 
context.write(aab, 1);
Wanted 3 times:
-> at com.tistory.devyongsik.WordCountMapperTest.testWithMockito(WordCountMapperTest.java:29)
But was 2 times:
-> at com.tistory.devyongsik.WordCountMapper.map(WordCountMapper.java:23)

at com.tistory.devyongsik.WordCountMapperTest.testWithMockito(WordCountMapperTest.java:29)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:73)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:46)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:180)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:41)
at org.junit.runners.ParentRunner$1.evaluate(ParentRunner.java:173)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
at org.junit.runners.ParentRunner.run(ParentRunner.java:220)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:49)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)



aab가 3번 나오기를 원했는데 2번만 나왔다는거죠..

그럼 MRUnit은 어떨까요..

MRUnit은 클라우데라(cloudera)에서 배포하는 하둡(CDH)을 받아보시면
그안에 들어있는 라이브러리입니다.

testWithMRUnit 메서드를 보시면 이 메서드에서는 우선 Mapper 객체를 생성하고
MRUnit에서 제공되는 MapDriver 클래스를 사용하여 테스트를 진행합니다.

테스트 데이터는 withInput을 통해 넣고
검사는 withOutput으로 합니다.
검사문을 withInput, withOutput으로 정해놓고 실행은 runTest메서드로 실행을 합니다.

개인적으로 결과의 직관적인 모습으로는 MRUnit이 더 좋아보이네요..

그럼 MRUnit의 테스트케이스 실패는 어떤 모양일까요..
withOutput메서드를 아래와 같이 고쳤을 때의 결과입니다.

driver.withOutput(new Text("aab"), new IntWritable(1));
driver.withOutput(new Text("bbc"), new IntWritable(1));
        driver.withOutput(new Text("aab"), new IntWritable(2));


2011. 7. 31 오후 10:45:34 org.apache.hadoop.mrunit.TestDriver lookupExpectedValue
심각: Matched expected output (aab, 1) but at incorrect position 2 (expected position 0)
2011. 7. 31 오후 10:45:34 org.apache.hadoop.mrunit.TestDriver validate
심각: Missing expected output (aab, 2) at position 2


mapper의 일이 분석 할 데이터를 reduce 할 수 있도록 
분류하는 작업을 한다고 생각하면 MRUnit이 mapper 테스트의 중점을 postion으로 두고 있는 것도
어느정도 이해가 갑니다.

위에서 position 2 라는 것은 3번째 작성 된 driver.withOutput(new Text("aab"), new IntWritable(1));
이 문장을 뜻하는 것으로 보입니다. 제일 처음이 0이겠죠... 결국 심각: Missing expected output (aab, 2) at position 2
라는 것은 3번째 작성된 withOutput 문장에서 잘못된 output이 나왔다.. 라는 것이고.. 그 위에 있는 에러문장은 
실제적으로 1이 나와야하지만.. 뭔가 잘못나왔다.. 라고 하는 것 같죠...

검증자체는 MRUnit이 굉장히 타이트한 것 같습니다. ouput의 순서까지도 검증하니까요..

그러고보니, Mockito와 MRUnit이 테스트의 검증 데이터를 바라보는
관점이 조금 다른 것 같습니다.

다음엔 Reduce쪽을 써볼게요...