Issue
I have a service that updates an Rxjs Subject whenever a method in the service is called:
@Injectable()
export class AppAlertService implements IAppAlertService {
private readonly _alertBehaviourSubject: Subject<IAlertConfiguration> = new Subject<IAlertConfiguration>();
public get alertConfiguration(): Observable<IAlertConfiguration> {
return this._alertBehaviourSubject.asObservable();
}
public alertError(alertMessage: string, alertOptions?: IAlertOptions, alertCtaClickCallback?: AlertOptionCallback): void {
this._alertBehaviourSubject.next({
message: alertMessage,
options: alertOptions ? alertOptions : { displayCloseLogo: true },
type: 'error',
callback: alertCtaClickCallback
});
}
}
And I can see that in the application, it works. I've manually tested it. However, I'm currently trying to write a unit test, and I keep timing out. I've ran into this issue a few time but I've always been able to resolve it using fakeAsync or a done callback in my assertion.
The test is written as follows:
describe('AppAlertService', () => {
let subject: AppAlertService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AppAlertService]
})
.compileComponents()
.then(() => {
subject = TestBed.inject<AppAlertService>(AppAlertService);
});
});
describe('Given an alert error', () => {
beforeEach(() => {
subject.alertError('Some Mock Alert Message', { displayCloseLogo: true });
});
it('Then the config is mapped correctly', (done) => {
subject.alertConfiguration
.pipe(first())
.subscribe({
next: (config: IAlertConfiguration) => {
expect(config).toEqual(false);
done();
},
});
});
});
});
I can get this test to pass if I change Subject<T> over to BehaviourSubject<T> but I don't want it to fire on construction so I chose a subject. Also, the assertion is completely wrong -> configuration will never be a boolean.
I've tried BehaviourSubjects, fakeAsync, done() callbacks, I've moved the done callback around, I've resolved the call to subject.alertConfiguration to a Promise<T> and it still fails, I've increased the timeout to 30 seconds... I'm stumped.
EDIT!
Thanks to Ovidijus Parsiunas' answer below. I realised there's a race condition between the beforeEach hook and the test. I've managed to get the test working:
import { AppAlertService } from './app-alert.service';
import { TestBed } from '@angular/core/testing';
import { IAlertConfiguration } from '../types/alert-configuration.interface';
describe('AppAlertService', () => {
let subject: AppAlertService;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [AppAlertService]
});
subject = TestBed.inject<AppAlertService>(AppAlertService);
});
describe('Given an alert', () => {
describe('When a minimal config is provided', () => {
it('Then the config is mapped correctly', (done) => {
subject.alertConfiguration
.subscribe((result: IAlertConfiguration) => {
expect(result).toEqual({
callback: undefined,
message: 'Some Mock Alert Message',
options: {
displayCloseLogo: true
},
type: 'error'
});
done();
});
subject.alertError('Some Mock Alert Message', { displayCloseLogo: true });
});
});
describe('Given an alert with a callback attached to the parameters', () => {
describe('When invoking the callback', () => {
const mockCallBack = jest.fn();
beforeEach(() => {
jest.spyOn(mockCallBack, 'mockImplementation');
});
it('Then the callback can be called', (done) => {
subject.alertConfiguration
.subscribe((result: IAlertConfiguration) => {
const resultCallback = result.callback as () => any;
resultCallback().mockImplementation();
done();
});
subject.alertError('Some Mock Alert Message', { displayCloseLogo: true }, () => mockCallBack);
});
it('Then the function is called once', () => {
expect(mockCallBack.mockImplementation).toHaveBeenCalled();
});
});
});
});
});
Solution
There are few major red flags within your example:
The code that is triggering your subscriber is inside a
beforeEachinstead of being inside the actualitunit test scope. It is important for code that invokes the functionality which you are testing to be in the body of a unit test as you want to test its results directly after it has executed and not decouple the assertion into a different function that is invoked by the test suite and not you. This is especially important when working with asynchronous code as you need to make sure things are executed at correct times andbeforeEachanditcould have a time differential. For completeness,beforeEachis used for setting up the state of the component/service that is being tested, to minimise the repeated test setup logic (given/arrange) and is most definitely not intended to be used to execute logic for the test.When testing pub sub code, you want to first subscribe the observable, and only later publish (
next) to it, so you will need to switch these two executions around which should produce the results you desire.
Answered By - Ovidijus Parsiunas
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.