RxJs тест на несколько значений из потока

Учитывая следующий класс:

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. Create a new ObjectStateContainer.
    • Assert that $isDirty is false.
  2. Invoke update on the ObjectStateContainer.
    • Assert that $isDirty is true.
  3. Invoke undoChanges on the ObjectStateContainer.
    • Assert that $isDirty is false.

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);
        });
});
});

person peinearydevelopment    schedule 11.05.2020    source источник
comment
Разве t => t !== t не всегда будет возвращать false? Вы имели в виду t => t !== this.t?   -  person Andrei Gătej    schedule 11.05.2020
comment
@ AndreiGătej Исправлена ​​опечатка. Спасибо что подметил это!   -  person peinearydevelopment    schedule 11.05.2020


Ответы (1)


Я бы выбрал мраморные тесты:

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, так это подписывается на данный наблюдаемый объект и превращает каждое событие value / error / complete в уведомление, каждое уведомление сопоставляется с временными рамками, в которые оно пришло (Исходный код).

Эти notifications (значение / ошибка / завершено) являются результатами задачи действия. Действие запланировано в очередь. Порядок, в котором они помещены в очередь, обозначается виртуальным временем.

Например, cold('----(b|)') означает: в frame 4 отправить value b и полное уведомление. Если вы хотите узнать больше о том, как эти действия и как они ставятся в очередь, вы можете ознакомиться с этим ответом SO.


В нашем случае мы ожидаем: a---b-----c, что означает:

  • frame 0: a (ложь)
  • frame 4: b (верно)
  • frame 10: c (ложь)

Откуда берутся эти номера кадров?

  • все начинается с frame 0 и что на данный момент класс едва инициализирован
  • cold('----(b|)) - будет выдавать t2 в кадре 4
  • cold('----------(c|)') - вызовет objectStateContainer.undoChanges() в кадре 10
person Andrei Gătej    schedule 11.05.2020