在我的角度项目中,我有一个服务,该服务用于状态管理以在组件之间共享一些数据,如下所示:
@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$
这个可观察对象的流中没有值。
这是什么问题?
答案 0 :(得分:0)
使用RxJS大理石测试您的设置可能非常困难。
第一个问题是您在订阅之前先向下游发送一些东西。 Remember,expectObservable()
同步订阅传递的可观察对象(在您的情况下为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