Angular Unit Test:如何使用大理石测试(rxjs / testing)测试此状态管理服务

时间:2019-12-24 03:32:04

标签: angular unit-testing rxjs jasmine-marbles rxjs-marbles

在我的角度项目中,我有一个服务,该服务用于状态管理以在组件之间共享一些数据,如下所示:

@Injectable({ providedIn: "root"})
export class NameStateService {

    private _filteredNames$: Subject<Name[]> = new Subject();
    private _filteredNamesObs$: Observable<Name[]>;

    constructor() {
        this._filteredNamesObs$ = this._filteredNames$.asObservable();
    }

    public updateFilteredNames(val: Name[]): void {
        this._filteredNames$.next(val);
    }

    public get filteredNames$(): Observable<BillingAccount[]> {
        return this._filteredNamesObs$;
    }
}

状态管理基于主题并且是可观察的,这是rxjs世界中的典型用法。

对于此服务的单元测试,我想使用rxjs/testing模块支持的大理石测试功能。解决方案如下:

describe("Name state service ", () => {
    let nameStateService: NameStateService;
    let scheduler: TestScheduler;

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                NameStateService
            ]
        });
        nameStateService = TestBed.get(NameStateService);
        scheduler = new TestScheduler((actual, expected) => expect(actual).toEqual(expected));
    });

    it("should be created", () => {
        expect(nameStateService).toBeTruthy();
    });

    it("should return a valid names observables", () => {
        scheduler.run(({ cold, hot, expectObservable }) => {
            const mockNames: Name[] = [{
                title: "title1",
                group: "group1"
            }]; 
            nameStateService.updateFilteredNames(mockNames);
            expectObservable(nameStateService.filteredNames$).toBe("-b", {b: mockNames});
        });
    });
})

但是第二个单元测试因以下错误而失败:Error: Expected $.length = 0 to equal 1.因此,意味着nameStateService.filteredNames$这个可观察对象的流中没有值。

这是什么问题?

1 个答案:

答案 0 :(得分:0)

使用RxJS大理石测试您的设置可能非常困难。

第一个问题是您在订阅之前先向下游发送一些东西。 RememberexpectObservable()同步订阅传递的可观察对象(在您的情况下为nameStateService.filteredNames$)。但是,由于您已经通过调用nameStateService.updateFilteredNames(mockNames)发送了数据,因此没有数据发送到那里。

您可以考虑让这两行更改位置,但是请记住,这是同步执行环境,所以这样做

expectObservable(nameStateService.filteredNames$).toBe("-b", {b: mockNames});
nameStateService.updateFilteredNames(mockNames);

也没有帮助,因为expectObservable()将订阅nameStateService.filteredNames$,然后它将读取所有值,但是由于没有值,因为在此之后将它们发送到一行中,所以{ {1}}数组为空。

为避免这种情况,您应该嘲笑actual可观察的对象。为此,您可以做两件事,但是它们都有自己的问题。因此,设置的第二个问题是您可以使nameStateService.filteredNames$cold成为可观察的,并使用它们代替hot为可观察的。

可以这样实现:

filteredNames$

TS2540出现此错误:由于它是只读属性,因此无法分配给'filteredNames $'。因为您没有nameStateService.filteredNames$ = hot('-b', { a: mockNames }); 的设置器。如果您要添加二传手,则这将破坏您将filteredNames$作为this._filteredNamesObs$ this._filteredNames$创建的私有财产的合同。

另一种方法是使用Jasmine间谍来模拟Subject(这是第三个问题),但是这种设置也存在问题。要嘲笑什么?整个this._filteredNames$?在这种情况下,您必须为服务的每个特定属性和功能创建间谍程序。还是模拟nameStateService属性?甚至更好,nameStateService._filteredNames$?但是嘲笑它们会导致其他人的行为有所不同,因为并非所有人都被嘲笑。

因此,我建议完全不使用TestScheduler,并这样编写测试:

nameStateService.filteredNames$

但是,可以提出另一种解决方案(如果您确实非常想使用大理石),则只需非常小心并知道您在做什么。您可以将it('should return a valid names observables', () => { const values: Name[][] = []; expect(values).toEqual([]); nameStateService.updateFilteredNames([{ group: 'G', title: 'Before subscribe' }]); expect(values).toEqual([]); nameStateService.filteredNames$.subscribe({ next(names) { values.push(names); } }); expect(values).toEqual([]); nameStateService.updateFilteredNames([{ group: 'G', title: 'After subscribe' }]); expect(values).toEqual([[{ group: 'G', title: 'After subscribe' }]]); }); 更改为_filteredNames$

ReplaySubject

这将导致任何后续的订阅者甚至在订阅流之前,都获得流向下发送的所有值。您只需删除一个传递给private _filteredNames$: Subject<Name[]> = new ReplaySubject(); 方法的char(-符号,然后您的传递测试将如下所示:

toBe