I have a coverage issue with my Angular/Karma tests.
I created a component that has a signUp() function
angularFireAuthSignOutSpyObj is a spy of this.auth from the component (Firebase Auth)
signUp() {
if (this.registrationForm.valid) {
this.auth.createUserWithEmailAndPassword
(
this.registrationForm.get('email')?.value,
this.registrationForm.get('password')?.value
)
.then(() => {
this.appMessage = "Account created !";
})
.catch((error) => {
this.appMessage = error.message;
});
} else {
this.appMessage = 'Submit logic bypassed, form invalid !'
}
}
I'm testing this component function with a karma test as is
it('should submit registration with form values', () => {
spyOn(component, 'signUp').and.callThrough();
angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.returnValue({
then: function () {
return {
catch: function () {
}
};
}
});
component.registrationForm.controls.email.setValue('[email protected]');
component.registrationForm.controls.password.setValue('ValidPass123');
component.registrationForm.controls.passwordCheck.setValue('ValidPass123');
expect(component.registrationForm.valid).toBeTruthy();
debugElement.query(By.css("button")).triggerEventHandler("click", null);
expect(component.signUp).toHaveBeenCalled();
expect(component.auth.createUserWithEmailAndPassword)
.toHaveBeenCalledWith(
component.registrationForm.controls.email.value,
component.registrationForm.controls.password.value)
// expect(component.appMessage).toEqual('Account created !');
});
As you can notice the last expect is commented out as it returns an Error: Expected undefined to equal 'Account created !'. This is because even though this.auth.createUserWithEmailAndPassword is defined in the mocked service angularFireAuthSignOutSpyObj, and is correctly called with the 2 expected arguments, I have no control over the then and catch functions that are defined.
They are defined so it won't trigger an error when trying to access it in the signUp() function. But what I would like to do is trigger the then(() => ...) and the catch(() => ...) so I can test/check that the app.message was correctly updated.
All the exceptions work until the last one. I feel like that I need to modify something in my createUserWithEmailAndPassword.and.returnValue to probably return something that triggers the then or the catch.
angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.returnValue({
then: function () {
return {
catch: function () {
}
};
}
});
Anyone has an idea on how I could test the actual auth.createUserWithEmailAndPassword result behaviour of my component ?
Thanks very much !
David
CodePudding user response:
I didn't see the code where you created the spy. Also a bit weird you're using promises instead of Observables. But, I'd look into spying on the method--not the class, and returning a promise that you control:
const resolveFunction;
const rejectFunction;
beforeEach(() => {
spyOn(component.auth, 'createUserWithEmailAndPassword').and.returnValue(new Promise((resolve, reject) => {
resolveFunction = resolve;
rejectFunction = reject;
})
}
Now from your tests you can control when the promise is rejected or resolved just by calling those functions:
it('test catch block', () => {
// some code
rejectFunction('some error object');
})
it('test then block', () => {
// some code
resolveFunction('some error object');
})
More info about creating promises manually
CodePudding user response:
Hey just wanted to post an update as I managed to do exactly what I needed. Thanks to @JeffryHouser for the heads up.
So basically my component initially expects a Promise from the query. If the results come back normal (UserCredentials) we simply update the appMessage string with a successful message. If not (catch) we return the error message.
Those are the changes I made on the test side in order to simulate the resolve (normal result of the promise, and below how to trigger the catch)
- Set the test as async with fakeAsync()
- Spy on every function that are used from the user click()
- Specified a return as Promise for the angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword function
- Simulate the async flow with tick()
- Detect the changes following the end of promise flow with fixture.detectChanges()
The appMessage item is correctly updated following the process
Here is the code!
Spy declaration
let angularFireAuthSignOutSpyObj: jasmine.SpyObj<any>;
...
beforeEach(async () => {
angularFireAuthSignOutSpyObj = jasmine.createSpyObj('AngularFireAuth',
['createUserWithEmailAndPassword']);
...
});
User credentials item
//Only setting the fields needed
export const testUserCredentials: UserCredential = {
user: {
providerData: [
{
email: '[email protected]',
}
]
}
}
Test
it('should submit registration with form values', fakeAsync(() => {
spyOn(component, 'signUp').and.callThrough();
angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.callFake(() => new Promise(
resolve => {
resolve(testUserCredentials);
})
);
component.registrationForm.controls.email.setValue('[email protected]');
component.registrationForm.controls.password.setValue('ValidPass123');
component.registrationForm.controls.passwordCheck.setValue('ValidPass123');
expect(component.registrationForm.valid).toBeTruthy();
debugElement.query(By.css("button")).triggerEventHandler("click", null);
expect(component.signUp).toHaveBeenCalled();
expect(component.auth.createUserWithEmailAndPassword)
.toHaveBeenCalledWith(
component.registrationForm.controls.email.value,
component.registrationForm.controls.password.value)
tick();
fixture.detectChanges();
expect(component.appMessage).toEqual('Account created : [email protected]');
}));
How to trigger the error instead of the resolve
angularFireAuthSignOutSpyObj.createUserWithEmailAndPassword.and.callFake(() => new Promise(() => {
throw {message: 'test purpose failure'};
}));
Updated register.component.ts
signUp() {
if (this.registrationForm.valid) {
let createdEmail: string | null | undefined;
this.auth.createUserWithEmailAndPassword
(
this.registrationForm.get('email')?.value,
this.registrationForm.get('password')?.value
)
.then((userCredential: UserCredential) => {
userCredential?.user?.providerData?.forEach(userInfo => {
createdEmail = userInfo?.email;
})
this.appMessage = "Account created : " createdEmail;
})
.catch((error) => {
this.appMessage = "Account creation failed : " error.message;
});
} else {
this.appMessage = 'Submit logic bypassed, form invalid !'
}
}