3

给定以下课程:

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每个阶段的值。目前,我已经看到了两种测试 observables 的方法,但我不知道如何用它们中的任何一种来做这个测试。我希望测试执行以下操作:

  1. 创建一个新的ObjectStateContainer.
    • 断言这$isDirty是错误的。
  2. 在 上调用更新ObjectStateContainer
    • 断言这$isDirty是真的。
  3. ObjectStateContainer.
    • 断言这$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);
        });
});
});
4

1 回答 1

4

我会选择大理石测试

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 并将每个值/错误/完成事件转换为通知,每个通知都与它到达的时间范围配对(源代码)。

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

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


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

  • frame 0:(a错误)
  • frame 4:(b真实)
  • frame 10:(c错误)

这些帧数来自哪里?

  • 一切都从那frame 0一刻开始,该类几乎没有初始化
  • cold('----(b|))t2- 将在第 4 帧发射
  • cold('----------(c|)')- 将objectStateContainer.undoChanges()在帧调用10
于 2020-05-11T15:28:29.833 回答