如何正确Mock Observable?ChangePasswordFacade.successful$返回false问题排查
ChangePasswordFacade.successful$ in Your Angular Test Let's break down why your current mocks aren't working and fix them step by step. The core issue here is that successful$ is a class property (an Observable derived from the store), not a method—so your attempt to spy on it like a method with spyOn(service, 'successful$') won't behave as expected. Plus, you're still using the real ChangePasswordFacade in your TestBed, which is tied to the NGRX store's initial state (where getSuccessful returns false by default).
Here are two reliable approaches to mock successful$ to return of(true):
Approach 1: Spy on the Property with spyOnProperty
Since successful$ is a property, use Jasmine's spyOnProperty instead of spyOn to override its value. You also need to spy on the facade before creating the component to ensure the component subscribes to your mocked observable:
describe('PasswordChangeModalComponent', () => { let component: PasswordChangeModalComponent; let fixture: ComponentFixture<PasswordChangeModalComponent>; let changePasswordFacade: ChangePasswordFacade; // Hold the facade instance early const childDefaultPopoverComponent = jasmine.createSpyObj('DefaultPopoverComponent', ['hideModal', 'showModal']); const childPasswordChangeFormComponent = jasmine.createSpyObj('PasswordChangeFormComponent', ['setFormControlsToEmpty']); const changePasswordFacadeSpy = jasmine.createSpyObj('ChangePasswordFacade', ['resetState']); configureTestSuite(() => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, StoreModule.forRoot({}), StoreModule.forFeature(CHANGEPASSWORD_FEATURE_KEY, changePasswordReducer, { initialState: changePasswordInitialState }) ], declarations: [PasswordChangeFormComponent, DefaultPopoverComponent, PasswordChangeModalComponent], providers: [Store, UserInformationFacade, ChangePasswordFacade] }); }); beforeEach(() => { // Grab the facade instance BEFORE component creation changePasswordFacade = TestBed.inject(ChangePasswordFacade); // Spy on the successful$ property and return our desired Observable spyOnProperty(changePasswordFacade, 'successful$').and.returnValue(of(true)); fixture = TestBed.createComponent(PasswordChangeModalComponent); component = fixture.componentInstance; fixture.detectChanges(); component.defaultPopoverComponent = childDefaultPopoverComponent; component.watchSuccessful = { unsubscribe: () => {} }; spyOn(component.watchSuccessful, 'unsubscribe'); }); describe('showPasswordChangeModal function ', () => { it('should call showModal and trigger hide logic when successful ', () => { spyOn(component, 'hidePasswordChangeModal'); component.showPasswordChangeModal(); expect(childDefaultPopoverComponent.showModal).toHaveBeenCalled(); // Assert the successful flow triggers the expected actions component.successful$.subscribe((successful: boolean) => { expect(successful).toBe(true); expect(component.hidePasswordChangeModal).toHaveBeenCalled(); expect(changePasswordFacade.resetState).toHaveBeenCalled(); expect(component.watchSuccessful.unsubscribe).toHaveBeenCalled(); }); }); }); });
Approach 2: Provide a Full Mock of ChangePasswordFacade
A cleaner approach (especially if you don't need the real NGRX store for this test) is to replace the real facade entirely with a mock. This avoids any dependency on the store's state:
describe('PasswordChangeModalComponent', () => { let component: PasswordChangeModalComponent; let fixture: ComponentFixture<PasswordChangeModalComponent>; const childDefaultPopoverComponent = jasmine.createSpyObj('DefaultPopoverComponent', ['hideModal', 'showModal']); const childPasswordChangeFormComponent = jasmine.createSpyObj('PasswordChangeFormComponent', ['setFormControlsToEmpty']); // Create a complete mock facade with our desired Observable and mocked methods const mockChangePasswordFacade = { successful$: of(true), initiated$: of(false), resetState: jasmine.createSpy('resetState'), changePassword: jasmine.createSpy('changePassword') }; configureTestSuite(() => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, StoreModule.forRoot({}) // You can remove the changePassword feature module entirely here ], declarations: [PasswordChangeFormComponent, DefaultPopoverComponent, PasswordChangeModalComponent], providers: [ Store, UserInformationFacade, // Replace the real facade with our mock { provide: ChangePasswordFacade, useValue: mockChangePasswordFacade } ] }); }); beforeEach(() => { fixture = TestBed.createComponent(PasswordChangeModalComponent); component = fixture.componentInstance; fixture.detectChanges(); component.defaultPopoverComponent = childDefaultPopoverComponent; component.watchSuccessful = { unsubscribe: () => {} }; spyOn(component.watchSuccessful, 'unsubscribe'); }); describe('showPasswordChangeModal function ', () => { it('should call showModal and trigger hide logic when successful ', () => { spyOn(component, 'hidePasswordChangeModal'); component.showPasswordChangeModal(); expect(childDefaultPopoverComponent.showModal).toHaveBeenCalled(); // Verify the successful Observable triggers the component's logic component.successful$.subscribe(() => { expect(component.hidePasswordChangeModal).toHaveBeenCalled(); expect(mockChangePasswordFacade.resetState).toHaveBeenCalled(); expect(component.watchSuccessful.unsubscribe).toHaveBeenCalled(); }); }); }); });
Why Your Previous Attempts Failed
- Using
spyOnfor a property:spyOnis designed for methods, not properties. You needspyOnPropertyto override the value of an Observable property. - Timing issues: You were creating the component first, then trying to spy on the facade—by that point, the component had already subscribed to the original
successful$from the real store-backed facade. - Real facade dependency: The real
ChangePasswordFacadepulls from the NGRX store's initial state, which returnsfalseforgetSuccessful, overriding your mock attempts.
内容的提问来源于stack exchange,提问作者user372225

