You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确Mock Observable?ChangePasswordFacade.successful$返回false问题排查

Fixing the Mock of 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 spyOn for a property: spyOn is designed for methods, not properties. You need spyOnProperty to 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 ChangePasswordFacade pulls from the NGRX store's initial state, which returns false for getSuccessful, overriding your mock attempts.

内容的提问来源于stack exchange,提问作者user372225

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:34:51