5

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

@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$;
    }
}

状态管理基于 subject 和 observable,这是 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$,这个 observable 在它的流中没有值。

这里有什么问题?

4

1 回答 1

10

您的设置可能很难使用 RxJS 弹珠进行测试。

第一个问题是您在订阅之前将一些内容发送到流中。请记住expectObservable()同步订阅传递的 observable(在您的情况下nameStateService.filteredNames$)。但是,没有数据发送到那里,因为您已经通过调用发送了它nameStateService.updateFilteredNames(mockNames)

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

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

也无济于事,因为expectObservable()会订阅nameStateService.filteredNames$,然后它将读取所有值,但是由于在此之后在一行中发送它们时没有值,因此actual数组将为空。

为避免这种情况,您应该模拟您的nameStateService.filteredNames$observable。要做到这一点,你可以做两件事,但他们都有自己的问题。因此,您设置的第二个问题是您可以制作coldhotObservable 并使用它们而不是filteredNames$Observable。

这可以这样实现:

nameStateService.filteredNames$ = hot('-b', { a: mockNames });

TS2540 出现此错误:无法分配给“filteredNames$”,因为它是只读属性。因为你没有 setter for filteredNames$. 如果您要添加 setter,那么这将打破您拥有this._filteredNamesObs$作为私有财产的合同,该财产是从this._filteredNames$ Subject.

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

所以,我建议根本不要使用 TestScheduler,并像这样编写你的测试:

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

这将导致任何后续订阅者在订阅之前获取所有沿流发送的值。您只需要删除一个-传递给toBe方法的 char ( 符号,您的通过测试将如下所示:

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});
  });
});
于 2019-12-30T14:18:01.037 回答