如何为含BehaviorSubject的RoleService创建Mock解决测试报错
roleService.isSiteAdmin$ is undefined in Angular Component Tests The error you're seeing happens because your RoleServiceMock doesn't properly replicate the public API of the real RoleService—specifically, it's missing all the observable properties like isSiteAdmin$ that your MainPageComponent subscribes to in its constructor.
Let's build a comprehensive mock that fully mimics the RoleService behavior, which you can reuse across all components that depend on this service.
Step 1: Create a Complete RoleServiceMock
This mock will replicate all the BehaviorSubject-backed observables from the original service, plus helper methods to dynamically adjust role values during tests (for more control over test scenarios):
import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; @Injectable() export class RoleServiceMock { // Replicate all observable properties from the real RoleService private isSiteAdminSubject = new BehaviorSubject<boolean>(false); public isSiteAdmin$ = this.isSiteAdminSubject.asObservable(); private isEditorSubject = new BehaviorSubject<boolean>(false); public isEditor$ = this.isEditorSubject.asObservable(); private isUserSubject = new BehaviorSubject<boolean>(true); public isUser$ = this.isUserSubject.asObservable(); private isProjectApproverSubject = new BehaviorSubject<boolean>(false); public isProjectApprover$ = this.isProjectApproverSubject.asObservable(); private isUsecaseApproverSubject = new BehaviorSubject<boolean>(false); public isUsecaseApprover$ = this.isUsecaseApproverSubject.asObservable(); private isActorApproverSubject = new BehaviorSubject<boolean>(false); public isActorApprover$ = this.isActorApproverSubject.asObservable(); private isScenarioApproverSubject = new BehaviorSubject<boolean>(false); public isScenarioApprover$ = this.isScenarioApproverSubject.asObservable(); private isPolicyApproverSubject = new BehaviorSubject<boolean>(false); public isPolicyApprover$ = this.isPolicyApproverSubject.asObservable(); private isFtpApproverSubject = new BehaviorSubject<boolean>(false); public isFtpApprover$ = this.isFtpApproverSubject.asObservable(); private isAdminSubject = new BehaviorSubject<boolean>(false); public isAdmin$ = this.isAdminSubject.asObservable(); // Mock the original service's methods resetRoles(): void { // Reset all role subjects to false (match original service behavior) this.isSiteAdminSubject.next(false); this.isEditorSubject.next(false); this.isUserSubject.next(false); this.isProjectApproverSubject.next(false); this.isUsecaseApproverSubject.next(false); this.isActorApproverSubject.next(false); this.isScenarioApproverSubject.next(false); this.isPolicyApproverSubject.next(false); this.isFtpApproverSubject.next(false); this.isAdminSubject.next(false); } setUserRoles(): void { // Leave this empty by default—we'll control role values manually in tests // If you need to replicate the original logic (e.g., reading from localStorage), you can add it here } // Helper methods to dynamically set role values during tests setIsSiteAdmin(value: boolean): void { this.isSiteAdminSubject.next(value); } setIsEditor(value: boolean): void { this.isEditorSubject.next(value); } setIsUser(value: boolean): void { this.isUserSubject.next(value); } // Add similar helper methods for all other roles as needed }
Step 2: Update Your Test Configuration
You already have the mock referenced in your TestBed providers, but make sure you're using the new complete mock. Then, you can inject the mock instance into your tests to control role values:
let component: MainPageComponent; let fixture: ComponentFixture<MainPageComponent>; let roleServiceMock: RoleServiceMock; // Add this to hold the mock instance fdescribe('MainPageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ // ... your existing declarations ], providers: [ // ... your existing providers { provide: RoleService, useClass: RoleServiceMock }, // Ensure this points to your new mock ], imports: [ // ... your existing imports ], schemas: [ NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA ] }).compileComponents(); })); beforeEach(async(() => { fixture = TestBed.createComponent(MainPageComponent); component = fixture.componentInstance; // Get the mock instance from the TestBed roleServiceMock = TestBed.inject(RoleService) as unknown as RoleServiceMock; fixture.detectChanges(); // Trigger initial change detection })); it('should create the app', () => { expect(component).toBeTruthy(); }); // Example test: Verify menu options are enabled for site admins it('should enable other menu options when user is a site admin', () => { // Set the role value roleServiceMock.setIsSiteAdmin(true); // Trigger change detection to update the component's subscriptions fixture.detectChanges(); // Assert the expected behavior expect(component.isOtherMenuOptionDisabled).toBe(false); }); });
Key Notes
- Mock Completeness: Always ensure your mock replicates every public property/method that components use from the real service. Missing properties will throw
undefinederrors, just like you saw. - Controllable Tests: Using helper methods like
setIsSiteAdminlets you easily test different role scenarios without relying on localStorage or other external dependencies. - Reusability: This mock works for any component that depends on
RoleService—just add it to the component's test providers and use the helper methods to adjust roles as needed.
内容的提问来源于stack exchange,提问作者tathai96

