본문 바로가기
Rx

RxJava 테스팅과 Flowable-2(TestObserver 클래스)

by 봄석 2019. 1. 1.

본 내용은 필자가 학습한 내용을 정리하는 내용입니다.

대부분 의 내용이 아래 책의 내용이므로 원서를 구매해서 직접보시는걸 추천드립니다!

RxJava 프로그래밍 리액티브 프로그래밍 기초부터 RxAndroid까지 한 번에

유동환 , 박정준 지음 | 한빛미디어 | 2017년 09월 04일 출간

http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9788968488658&orderClick=LAV&Kc=


저자님의 블로그

https://brunch.co.kr/@yudong#info





테스팅과 Flowable

이제 JUnit 5 기반으로 RxJava 테스트 코드를 작성합니다 .

앞에서 부터 RxJava의 연산자 들의 마블 다이어 그램을 구현하면서 자주 사용했던 

Shape클래스의 getShape() 메소드입니다. Observable에 넣었을 때 정상 동작하는지 확인합니다.


RxJava의 테스트코드 예

@DisplayName("test getShpae() Observable")
@Test
void testGetShapeObservable(){
            String[] data ={"1","2-R","3-T"};
            Observable<String> source= Observable.fromArray(data)
                    .map(Shape::getShape);
            
            String[] expeceted={Shape.BALL,Shape.RECTANGLE,Shape.TRIANGLE};
            List<String> actual=new ArrayList<>();
            source.doOnNext(Log::d)
            .subscribe(actual::add);
            assertEquals(Arrays.asList(expeceted),actual);
 

 }


source는 data 각 값을 Observable에 넣은 후 Shape.getShape()를 호출하여 매핑합니다.

데이터 예상 결과는 expected 인자에 저장했습니다. source에서 발행하는 데이터 원본은 doOnNest() 함수에서 확인할 수 있으며 실제 값은 actual 변수에 저장합니다.


마지막으로 assertEquals() 메소드를 통해 내가 예상했던 데이터 (expected)와 실제 데이터 (actual)이 일치하는지 확인합니다.


doOnNext() 함수로 콘솔에 출력한 데이터는 아래와 같습니다.

main | debug = BALL
main | debug = RECTANGLE
main | debug = TRIANGLE



JUnit의 테스트결과를 확인할 때는 왼쪽의 녹색 막대가 100% 채워져있다면 성공했다는 뜻입니다.


지금까지 순수하게 JUnit에 기반을 두고 테스트 코드를 작성했습니다. 하지만 RxJava는 뜨거운 Observable과 같이 비동기로 동작하는 코드를 테스트 할 수 있어야 하므로 JUnit에서 제공하는 테스트 메소드 만으로는 한계가 있습니다. 좀 더 쉬운방법은 없을까요 ?




TestObserver 클래스

RxJava에서 제공하는 TestObserver 클래스를 활용한 방법을 알아보기 위해 testGetShapeObservable() 테스트 코드를 좀 더 쉽게 바꾸어 보겠습니다.

RxJava 2.x에 새롭게 추가된 방식이기도 합니다.


TestObserver를 활용한 테스트 예

@RunWith(JUnitPlatform.class)
public class TestObserverExample {
    
    @DisplayName("#1: using TestObserver for Shape.getShape()")
    @Test
    void testGetShapeUsingTestObserver(){
        String[] data ={"1","2-R","3-T"};
        Observable<String> source= Observable.fromArray(data)
                .map(Shape::getShape);
        
        String[] expected={Shape.BALL,Shape.RECTANGLE,Shape.TRIANGLE};
        source.test().assertResult(expected);
    }
}



앞서 작성한 JUnit 5 기반의 테스트 코드와 다른 점은 test()와 assertResult() 함수입니다.

test() 함수는 TestObserver 객체를 리턴합니다.


RxJava 2.x에 새로 추가된 test() 함수의 코드는 아래와 같습니다.


test() 함수의 코드

public final TestObserver<T> test(){
    TestObserver<T> ts=new TestObserver<T>();
    subscribe(ts);
    return ts;

}


단지 TestObserver 객체를 만들어거 그것을 구독하는 동작만 합니다.


TestObserver 클래스의 주요 함수는 다음과 같습니다


- assertResult() : 예상된(expected) 결과와 실제(actual) 결과를 비교하는 메소드 입니다.

JUnit의 assertEquals() 메소드와 같습니다

- assertFailure() : Observable에서 기대했던 에러(onError 이벤트)가 발생하는지 확인하는 코드입니다. 만약 기대했떤 에러가 발생하지 않으면 테스트 코드 실행은 실패합니다.

-assertFailureAndMessage() : 기대했떤 에러 발생 시 에러 메시지까지 확인할 수 있습니다.

- awaitDone() : interval() 함수처럼 비동기로 동작하는 Observable 코드를 테스트 할 수 있습니다.

- assertComplete() : Observable을 정상적으로 완료(onComplete 이벤트) 했는지 확인합니다.



아래는 assertFailure() 함수를 활용하는 예제입니다

총 3개의 값을 넣어서 앞 두번째 까지는 정상으로 발행하고 마지막 값에서 기대했던 예외가 발생하는지 확인합니다.


assertFailure() 함수의 활용 예

 @DisplayName("assertFailure() eample")
    @Test
    void assertFailureExample(){
        String[] data ={"100","200","&300"};
        Observable<Integer> source=Observable.fromArray(data)
                .map(Integer::parseInt);
        
        source.test().assertFailure(NumberFormatException.class, 100,200);
}



Integer 클래스의 parseInt() 메소드는 정수 이외의 값을 잘못 입력해서 정수로 변환할 수 없을때 NumberFormatException이 발생합니다. 따라서 Observable에 비정상 데이터가 들어왔을 때 앞 예외가 발생하는지 알아봅니다. 

첫 번째 데이터와 두 번째 데이터는 정상적으로 처리하고 세 번째 데이터는 NumberFormatException 이 발생하고 onError 이벤트로 종료되었습니다.




assertFailureAndMessage() 함수

assertFailureAndMessage() 함수는 assertFailure() 함수와 동일하지만 에러 메시지를 확인하기 위해 message인자가 추가된 형태입니다. 사용자 정의 예외를 활용하는 경우 사전에 유용하게 테스트해 볼 수 있습니다.

@DisplayName("assertFailureAndMessage() eample")
    @Test
    void assertFailureAndMessage(){
        String[] data ={"100","200","&300"};
        Observable<Integer> source=Observable.fromArray(data)
                .map(Integer::parseInt);
        
        source.test().assertFailureAndMessage(NumberFormatException.class,"for input string:  \"%300\"",100,200);
    }



세번째 값인 '%300'에 대한 에러메시지까지 맞는지 확인합니다. 만약 에러 메시지가 다른 경우 테스트 코드를 실행하면 다음 에러를 확인할 수 있습니다.


마지막은 assertComplete() 함수입니다


assertComplete()함수

테스트하는 Observable이 정상적으로 완료되었는지 확인합니다.


assertComplete() 함수 활용 예

@DisplayName("assertComplete() example")
    @Test
    void assertComplete() { 
        Observable<String> source = Observable.create(
            (ObservableEmitter<String> emitter) -> { 
                emitter.onNext("Hello RxJava");
                emitter.onComplete();
            });
        source.test().assertComplete();
    }

Colored by Color Scripter


source 의 Observable은 onComplete() 함수를 명시적으로 호출했습니다. 

TestObserver 에서 source Observable이 onComplete 알림을 보냈는지 확인한다는 뜻입니다.


참고로 assertComplete() 의 반대 함수로 assertNotComplete() 함수도 제공합니다.




JUnit과 그레이들로 RxJava 테스트코드 작성하기

JUnit은 자바언어 에서 가장 널리 사용하는 테스트 프레임워크입니다. JUnit 기반으로 RxJava 의 테스트 코드를 작성하는 이유는 IDE와의 밀접함을 유지하기 위함입니다. 만약 RxJava 만으로 테스트 코드를 작성한다면, 실행하기 위해서 일일이 main() 메소드를 만들고 필요한 메소드를 호출해야 합니다. 하지만 JUnit은 IDE에 맞는 Runner 클래스를 제공하기 때문에 테스트를 손쉽게 호출하고 결과를 녹색 막대 같은 형태를 이용해 가시적으로 확인할 수 있습니다.


그레들을 활용하면 테스트 코드를 그룹화 할 수 있습니다. 그레이들 빌드의 경우 /src/main/java에는 소스코드를 넣고 /src/test/java에 테스트 코드를 넣습니다. 또한 테스트 코드나 일반 소스 코드에 맞게 원하는 라이브러리를 선택적으로 넣을 수도 있습니다. 테스트 코드에 해당하는 dependency로는 testImplementation과 testRuntime 등이 있습니다.


또한 JUnit은 라이프 사이클이 있습니다. 단일 Observable을 테스트하는데는 RxJava의 TestObserver클래스로 충분하겠지만 여러 테스트 케이스를 위한 사전 단계(@Before 등)

혹은 사후 단계 (@After등)를 활용하면 테스트 코드를 준비 코드와 실제 테스트 코드로 보기 좋게 구분하여 주요 로직을 구현하는데 집중할 수 있습니다.


댓글