Query asked by user
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 BehaviourSubject
s, 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();
});
});
});
});
});
Answer we found from sources
There are few major red flags within your example:
-
The code that is triggering your subscriber is inside a
beforeEach
instead of being inside the actualit
unit 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 andbeforeEach
andit
could have a time differential. For completeness,beforeEach
is 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
This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0