如何测试订阅内部方法

How to test subscription inside method

考虑这个 angular 组件:

export class CheckoutComponent {
  constructor(private paymentService: PaymentService, private paymentModalService: PaymentModalService) {}

  public onCheckout(): void {
    const paymentStatus$: Observable<string> = this.paymentService.getStatus();

    const paymentRequired$ = paymentStatus$.pipe(map(paymentStatus => paymentStatus === 'INCOMPLETE'));

    paymentRequired$.subscribe(() => {
      this.paymentModalService.open();
    });
  }
}

我正在尝试编写 jasmine 测试以确认 paymentModalService.open() 被调用:

import { cold } from 'jasmine-marbles';

describe('CheckoutComponent', () => {
  let component: CheckoutComponent;
  let mockPaymentService: jasmine.SpyObj<PaymentService>;
  let mockPaymentModalService: jasmine.SpyObj<PaymentModalService>;

  beforeEach(() => {
    mockPaymentService = jasmine.createSpyObj(['getStatus']);
    mockPaymentModalService = jasmine.createSpyObj(['open']);
    component = new CheckoutComponent(mockPaymentService, mockPaymentModalService);
  });

  it('should open payment modal if required', () => {
    mockPaymentService.getStatus.and.returnValue(cold('a', { a: 'INCOMPLETE' }));
    component.onCheckout();

    expect(mockPaymentModalService.open).toHaveBeenCalled(); // FAIL: was never called
  });
});

但是显然从未调用过 open 方法。

在断言该方法已被调用之前,有什么方法可以等待冷测试 observable 完成发出吗?

我试过使用 fakeAsynctick,但似乎都没有用。我意识到它适用于 of() 而不是 TestColdObservable,但我希望对可观察行为进行更精细的控制。我真的不打算完成 getStatus() observable。

您的主要问题是:

I would like more granular control of observable behavior. I don't really intend to complete the getStatus() observable.

如果你不想完成它那么它本质上不是冷观察,它是热观察,这意味着你可以创建一个主题并通过

   var sub = new Subject();
    mockPaymentService.getStatus.and.returnValue(sub.asObservable());
    // run method and let it subscribe 
    component.onCheckout();
    
// fire it
    sub.next('a', { a: 'INCOMPLETE' });
 expect(mockPaymentModalService.open).toHaveBeenCalled();

我建议您阅读有关如何编写组件测试用例的文档: Angular guide to writing test cases

问题: 在您当前的测试方法中,您将无法 运行 组件生命周期,而是 运行 手动安装它们并跳过实际测试用例。 它适用于服务,但不适用于组件。

让我给你一些建议,这不是你问题的重点,但无论如何都会有所帮助。

我的建议是将逻辑从组件移至服务。

查看您的代码,CheckoutComponent 似乎需要一个可观察对象 paymentRequired$,它会在需要付款时发出通知。

我们假设 this.paymentService.getStatus() 是对后端 API 的调用,returns 是付款状态。在这种情况下,我将在服务中做的事情是这样的

export class PaymentService {
  constructor(http, ...)

  // Private Subjects - using Subject to notify allows multicasting
  private _paymentRequired$ = new Subject<boolean>();

  // Public API Streams
  public paymentRequired$ = this._paymentRequired$.asObservable();

  // Public API methods
  public getStatus() {
    this.http.get(....).pipe(
      tap({
        next: resp => this._paymentRequired$.next(resp === 'INCOMPLETE'),
        error: err => {// manage the error case}
      })
    )
  }
}

如果你像这样将逻辑移动到服务中,那么 CheckoutComponent 可以简单地在其 ngOnInit 方法中订阅 paymentService.paymentRequired$ observable,而测试只能处理使用服务而不是组件。

特别是测试必须以某种方式实例化服务,订阅 paymentRequired$ 可观察对象,调用 getStatus() 并检查 paymentRequired$ 是否已通知。

阅读有关 component marble tests 的文档,我发现我需要像这样刷新测试调度程序:

import { cold, getTestScheduler } from 'jasmine-marbles';

it('should open payment modal if required', () => {
  mockPaymentService.getStatus.and.returnValue(cold('a', { a: 'INCOMPLETE' }));
  component.onCheckout();
  getTestScheduler().flush(); // <=== prompt the scheduler to execute all of its queued actions

  expect(mockPaymentModalService.open).toHaveBeenCalled(); // SUCCESS
});