RxJs 测试流中的多个值

标签 rxjs rxjs-marbles

给定以下类:

import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';

export class ObjectStateContainer<T> {
    private currentStateSubject = new BehaviorSubject<T>(undefined);
    private $currentState = this.currentStateSubject.asObservable();

    public $isDirty = this.$currentState.pipe(map(t => t !== this.t));

    constructor(public t: T) {
        this.update(t);
    }

    undoChanges() {
        this.currentStateSubject.next(this.t);
    }

    update(t: T) {
        this.currentStateSubject.next(t);
    }
}

我想编写一些测试来验证 $isDirty 包含我在执行各种函数调用后期望的值。具体来说,我想测试创建一个变量,更新它,然后撤消更改并验证每个阶段的 $isDirty 的值。目前,我已经看到了两种测试可观察对象的方法,但我不知道如何用它们中的任何一种进行测试。我希望测试执行以下操作:

  1. 创建一个新的 ObjectStateContainer
    • 断言 $isDirty 为假。
  2. 调用 ObjectStateContainer 的更新。
    • 断言 $isDirty 为真。
  3. ObjectStateContainer 调用 undoChanges。
    • 断言 $isDirty 为假。

import { ObjectStateContainer } from './object-state-container';
import { TestScheduler } from 'rxjs/testing';

class TestObject {
}

describe('ObjectStateContainer', () => {
    let scheduler: TestScheduler;

    beforeEach(() =>
        scheduler = new TestScheduler((actual, expected) =>
        {
            expect(actual).toEqual(expected);
        })
    );

    /*
        SAME TEST AS ONE BELOW
        This is a non-marble test example.
    */
    it('should be constructed with isDirty as false', done => {
        const objectStateContainer = new ObjectStateContainer(new TestObject());
        objectStateContainer.update(new TestObject());
        objectStateContainer.undoChanges();

        /*
            - If done isn't called then the test method will finish immediately without waiting for the asserts inside the subscribe.
            - Using done though, it gets called after the first value in the stream and doesn't wait for the other two values to be emitted.
            - Also, since the subscribe is being done here after update and undoChanges, the two previous values will already be gone from the stream. The BehaviorSubject backing the observable will retain the last value emitted to the stream which would be false here.
            I can't figure out how to test the whole chain of emitted values.
        */
        objectStateContainer
            .$isDirty
            .subscribe(isDirty => {
                expect(isDirty).toBe(false);
                expect(isDirty).toBe(true);
                expect(isDirty).toBe(false);
                done();
            });
    });

    /*
        SAME TEST AS ONE ABOVE
        This is a 'marble' test example.
    */
    it('should be constructed with isDirty as false', () => {
        scheduler.run(({ expectObservable }) => {
            const objectStateContainer = new ObjectStateContainer(new TestObject());
            objectStateContainer.update(new TestObject());
            objectStateContainer.undoChanges();

         /*
            - This will fail with some error message about expected length was 3 but got a length of one. This seemingly is happening because the only value emitted after the 'subscribe' being performed by the framework is the one that gets replayed from the BehaviorSubject which would be the one from undoChanges. The other two have already been discarded.
            - Since the subscribe is being done here after update and undoChanges, the two previous values will already be gone from the stream. The BehaviorSubject backing the observable will retain the last value emitted to the stream which would be false here.
            I can't figure out how to test the whole chain of emitted values.
        */
            const expectedMarble = 'abc';
            const expectedIsDirty = { a: false, b: true, c: false };
            expectObservable(objectStateContainer.$isDirty).toBe(expectedMarble, expectedIsDirty);
        });
});
});

最佳答案

我会选择大理石测试:

scheduler.run(({ expectObservable, cold }) => {
  const t1 = new TestObject();
  const t2 = new TestObject();
  const objectStateContainer = new ObjectStateContainer(t1);

  const makeDirty$ =  cold('----(b|)', { b: t2 }).pipe(tap(t => objectStateContainer.update(t)));
  const undoChange$ = cold('----------(c|)', { c: t1 }).pipe(tap(() => objectStateContainer.undoChanges()));
  const expected = '        a---b-----c';
  const stateValues = { a: false, b: true, c: false };

  const events$ = merge(makeDirty$, undoChange$);
  const expectedEvents = '  ----b-----(c|)';

  expectObservable(events$).toBe(expectedEvents, { b: t2, c: t1 });
  expectObservable(objectStateContainer.isDirty$).toBe(expected, stateValues);
});

expectObservable 所做的是订阅给定的 observable 并将每个值/错误/完成事件转换为通知,每个通知都与时间范围配对它已经到了(Source code)。

这些通知(值/错误/完成)是操作任务的结果。一个 Action 被安排到一个队列中。他们排队的顺序由虚拟时间指示。

例如,cold('----(b|)') 表示:在 frame 4 发送 value b 并且一个完整的通知。 如果您想了解更多关于这些操作如何以及它们如何排队的信息,您可以查看此 SO answer .


在我们的例子中,我们期望:a---b-----c,这意味着:

  • 第 0 帧:a(假)
  • 第 4 帧:b(真)
  • 第 10 帧:c(假)

这些帧数从何而来?

  • 一切都从 frame 0 开始,此时类几乎没有初始化
  • cold('----(b|)) - 将在第 4 帧发出 t2
  • cold('------------(c|)') - 将在 10 帧调用 objectStateContainer.undoChanges()

关于RxJs 测试流中的多个值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61730747/

相关文章:

javascript - 为什么 rxjs 6 弹珠文档说 "--(abc)-|"在第 8 帧完成?

Angular 单元测试 : how to use marble testing (rxjs/testing) to test this state management service

angular - Observable 在 Angular 中使用 Http 的好处

angular - 将 max 与选择器一起使用

Angular RxJs : Poll HTTP request until timeout or positive response from server

javascript - 如何在 rxjs 和 axios 中永远重复订阅

javascript - 基于回调返回 Observable

rxjs - 延迟测试 NGRX 效果