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. Создать новый 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);
        });
});
});

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
Другие вопросы по тегам