FE

[RxJS] 테스트코드 짜는 방법

개발공주 2022. 5. 19. 19:26
728x90
Mocha.js와 RxJS 테스트 도구를 통해 비동기 코드를 어떻게 테스트하는지 알아본다. <RxJS 반응형 프로그래밍> 책 내용 재정리

 

1. 동기 함수 테스트

순수함수 특징

  • 범위가 작고, 명확하게 정의된 매개변수 세 개정도를 가짐.
  • 예측 가능하고 일관된 출력을 냄
  • 전달된 인수로부터 결과가 바로 결정되는 결정론적인 특성이 있어, 테스트 결과는 인수에 달려 있음.
export const isNotEmpty = (input) => {
    return !!input && input.trim().length > 0;
};
import { expect } from "chai";
import { isNotEmpty } from "../9_1.js";

describe("기본적인 동기 테스트 코드", () => { 
    it("간단한 빈문자열 밸리데잇 함수", () => {
        expect(isNotEmpty("나는 이은진")).to.be.equal(true);
        expect(isNotEmpty(" ")).to.be.equal(false);
        expect(isNotEmpty(null)).to.be.equal(false);
        expect(isNotEmpty(undefined)).to.be.equal(false);
    });
});
  • it으로 묶인 테스트 블록들은 하나의 특정 작동의 특성을 지니도록 작성한다.
  • 긍정적, 부정적인 사용 사례에 대해 어서션 테스트를 한다. (* 어서션: 참거짓을 미리 가정함)

2. 비동기 함수 테스트

describe("검색결과 비동기 요청 테스트 코드 - should 추가", function () {
	it("위키피디아에 'reactive programming' 키워드 검색", function (done) {

        this.timeout(2000);
        const searchTerm = "reactive+programming";
        const url = `https://en.wikipedia.org/w/api.php?action=query`+
		`&format=json&list=search&utf8=1&srsearch=${searchTerm}`;

        const success = (results) => {
            // 성공 함수 설정
            expect(results)
                .to.have.property("query")
                .with.property("search")
                .with.length(10);
            done();
        };

        const error = (err) => {
            done(err);
        };

        getData2(url, success, error);
    });
});
  • done함수를 전달하면 Mocha가 중지되고 비동기 함수가 반환될 때까지 기다림.
  • done 함수를 포함하는 success, error 함수를 인자로 전달
describe("검색결과 비동기 요청 테스트 코드 - should 추가", function () {
    it("위키피디아에 'reactive programming' 키워드 검색", function () {
        // done함수를 사용하는 대신 Promise를 Mocha로 반환함

        const searchTerm = "reactive+programming";
        const url = `https://en.wikipedia.org/w/api.php` + 
		`?action=query&format=json&list=search&utf8=1&srsearch=${searchTerm}`;

        return getData3(url)
            .should.be.fulfilled
            .should.eventually.have.property("query")
            .with.property("search")
            .with.length(10);
    });
});
  • should.js api를 사용하여, should.be..fulfilled 처럼 promise 최종값에 어서션 테스트 진행
  • done을 통과하는 대신, mocha가 테스트 중인 promise 객체를 엔진에 반환하여, 지정된 기댓값을 충족하길 기대한다.

3. 반응형 스트림 테스트

describe("rxjs 연산자로 구성된 코드의 테스트를 짜봅시다", () => {
    it("동기 - 더하기 연산", () => {
        of(1, 2, 3, 4, 5, 6, 7, 8, 9)
            .pipe(reduce((total, delta) => total + delta))
            .subscribe((total) => {
                expect(total).to.equal(45);
            });
    });
});
  • 동기 옵저버블 테스트
describe("rxjs 연산자로 구성된 코드의 테스트를 짜봅시다", () => {
    it("1000ms 딜레이 - 더하기 연산", () => {
        from([1, 2, 3, 4, 5, 6, 7, 8, 9])
            .pipe(
                reduce((total, delta) => total + delta),
                delay(1000)
            )
            .subscribe((total) => {
                expect(total).to.equal(45);
            });
    });
    it("1000ms 딜레이 - 더하기 연산", () => {
        from([1, 2, 3, 4, 5, 6, 7, 8, 9])
            .pipe(
                reduce((total, delta) => total + delta),
                delay(1000)
            )
            .subscribe((total) => {
                expect(total).to.equal("성공한 거 맞아요?");
                // 에러는 발생하는데, 테스트는 통과한 것으로 나온다!!
                // 비동기 블록 실행이 완료되기도 전에 테스트가 완료되었다고 보고되는 게 문제.
            });
    });
});
  • 비동기 옵저버블 잘못된 예시

4. 스트림을 테스트에 용이하게 리팩토링하기

/**
 * 리팩토링 전 프로그램
 */
interval(200)
    .pipe(
        take(10),
        filter((num) => num % 2 === 0),
        map((num) => num * num),
        reduce((total, delta) => total + delta)
    )
    .subscribe(console.log);
  1. 옵저버블 파이프라인에서 비즈니스 로직 분리
  2. 소비자와 생산자 분리, 스트림 파이프라인 격리. 어서션 코드 주입 위함
  3. 스트림을 적절한 옵저버로 호출할 수 있는 함수로 래핑
/**
 * 리팩토링 후 프로그램
 */
const isEven = (num) => num % 2 === 0;
const square = (num) => num * num;
const add = (total, delta) => total + delta;

export const runInterval = (source$) =>
    source$.pipe(take(10), filter(isEven), map(square), reduce(add));
  • 인수를 만들어 비즈니스 로직에서 생산자(소스)와 구독자를 분리해줌.
  • 래핑 결과, 테스트 입력 인수를 전달할 수 있게 됨.
  • 코드 전체 경로에서 실행하는 데 요구되는 가능한 사용사례를 전부 감당할 수 있는 유연성 가짐 - ?
/**
 * 테스트에 최적화하여 리팩토링
 */
describe("옵저버블 테스트", function () {
    it("부가작용 없는 옵저버블 리팩토링", (done) => {
        this.timeout(2000); // 스트림을 완료할 수 있게 Mocha의 시간 초과 설정 늘림.

        runInterval(interval(200)).subscribe({
            next: (total) => expect(total).to.equal(120),
            err: (err) => assert.fail(err.message),
            complete: done(),
        });
        // 기댓값은 스트림 코드에서 분리되어, 테스트에 연결됨
    });
});
  1. 이렇게 하면 테스트 기본 시간(2000ms)를 초과할 것이라고 Mocha에게 항상 알려야 함.
  2. interval 같은 명시적인 시간값이 존재하는 코드에서도 테스트 속도를 빠르게 하려면 스케줄러 추가가 필요.

5. RxJS의 값 스케줄링

얄코 RxJS 스케줄러 관련글: https://www.yalco.kr/@rxjs/1-5/

it("동기 스케줄링", () => {
        let stored = [];
        let store = (state) => stored.push(state);
        let scheduler = queueScheduler;

        scheduler.schedule(store(1));
        scheduler.schedule(store(2));
        scheduler.schedule(store(3));
        scheduler.schedule(store(4));
        scheduler.schedule(store(5)); // schedule을 호출할 때마다 Subscription 객체 반환
        scheduler.flush();

        expect(stored).to.deep.equal([1, 2, 3, 4, 5]);
    });
  • 동기 스케줄링
it("비동기 스케줄러에 값 발행하기", (done) => {
        let temp = [];

        of(1, 2, 3, 4, 5)
            .pipe(observeOn(asyncScheduler), tap([].push.bind(temp)))
            .subscribe({
                next: (value) => {
                    console.log(value);
                    expect(temp).to.have.length(value);
                    expect(temp).to.contain(value);
                },
                error: (err) => console.error(err),
                complete: done(),
            });
    });
  • 비동기 스케줄러를 사용하도록 스트림을 구성하여 생산자가 방출한 값을 프록시함.
  • 구독 블록 전에 모든 값이 방출되게 함
  • 값을 방출하는 모든 비동기 부분에서 배열이 증가하에 어서션 테스트 수행
728x90