Reputation: 4942
Consider this angular component:
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();
});
}
}
I'm trying to write a jasmine test to confirm that paymentModalService.open()
was called:
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
});
});
However the open method was apparently never called.
Is there any way to wait for the cold test observable to complete emit before asserting that the method has been called?
I've tried using fakeAsync
and tick
but it does not either seem to help. I realize it works with of()
instead of a TestColdObservable
, but I would like more granular control of observable behavior. I don't really intend to complete the getStatus()
observable.
Upvotes: 1
Views: 1146
Reputation: 4942
Reading documentation on component marble tests I found that I need to flush the test scheduler like so:
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
});
Upvotes: 1
Reputation: 17762
Let me suggest you something, which is not really to the point of your question but can help anyways.
My suggestion is to move logic from the component to the service.
Looking at your code it seems that the CheckoutComponent
needs an observable, paymentRequired$
, which notifies when a payment is required.
Let's assume this.paymentService.getStatus()
is a call to a back end API, which returns the status of payments. In this case, what I would do in the service would be something like this
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}
})
)
}
}
If you move the logic to the service like this, then the CheckoutComponent
can simply subscribe to the paymentService.paymentRequired$
observable in its ngOnInit
method, and the the test can deal only with service and not the component.
In particular the test would have to instanciate somehow the service, subscribe to the paymentRequired$
observable, invoke getStatus()
and check if paymentRequired$
has notified.
Upvotes: 1
Reputation: 2270
Your main question is:
I would like more granular control of observable behavior. I don't really intend to complete the getStatus() observable.
If you do not want to complete it then it essentially is not a cold observable, its a hot observable, that would mean you can create a subject and pass
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();
I would suggest you go through the document of how to write component test-cases: Angular guide to writing test cases
Problem: In your current method of testing you won't be able to run component life-cyle, and instead would be running them manually and skip on real-life test cases. It is fine for service but not components.
Upvotes: 0