Unit testing for backend and services methods calls with Jasmine - javascript

I started working with tests, more specifically with Jasmine, and I'm having some difficulty to test if the backend and some services methods are being called.
So basically I'm calling the forgotPassword method when the formulary is submitted and I was wondering how can I properly check if the API (apiSendPasswordResetLink) and the services methods (showLoader, showAlert and navigateTo) are being called as expected.
async forgotPassword() {
try {
console.log("1");
this.loadingService.showLoader();
console.log("2");
await this.userService
.apiSendPasswordResetLink(this.form.value['email'])
.toPromise();
console.log("3");
this.utilitiesService.showAlert(`We've emailed you a link to reset your password. Please check your mail box and spam.`);
console.log("4");
delay(1500);
this.navigationService.navigateTo('/login/auth');
console.log('5')
} catch (err) {
this.utilitiesService.showAlert(err);
} finally {
this.loadingService.hideLoader();
}
}
The test:
it('should submit email to reset password after submitting formulary', () => {
component.form.setValue({
email: 'test#test.io',
});
const loadingService = TestBed.inject(LoaderService);
const userService = TestBed.inject(UserService);
const utilitiesService = TestBed.inject(UtilitiesService);
const navigationService = TestBed.inject(NavigationService);
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector('#button');
spyOn(component, 'forgotPassword').and.callThrough();
spyOn(loadingService, 'showLoader');
spyOn(userService, 'apiUserSendPasswordResetLink');
spyOn(utilitiesService, 'showAlert');
spyOn(navigationService, 'navigateTo');
// Submitting form
fixture.debugElement
.query(By.css('form'))
.triggerEventHandler('ngSubmit', null);
expect(component.form.valid).toEqual(true);
expect(button.disabled).toBeFalsy();
expect(component.forgotPassword).toHaveBeenCalled();
expect(loadingService.showLoader).toHaveBeenCalled();
expect(userService.apiUserSendPasswordResetLinkGet).toHaveBeenCalled();
expect(utilitiesService.showAlert).toHaveBeenCalled();
expect(navigationService.navigateTo).toHaveBeenCalled();
});
Every time I run and / or debug the test, I have the error
Expected spy navigateTo to have been called.
But the console never prints "3", which means showAlert is also not being called and I should also have the same error regarding showAlert spy to be called, but I don't.
I don't know if this problem has to do if the await call to the API or something else. I would like to know how can I fix it, so all the test can pass as expected.
Thank you!

When adding a spy on UserService#apiUserSendPasswordResetLink, without any spy strategy, it defaults to doing nothing. However, in your forgotPassword method, you are chaining the response of the call to a Promise wrapper and awaiting the resolution. Since apiUserSendPasswordResetLink is not invoked, I'm guessing that the promise is never resolved and the test gets stuck.
One simple way to resolve the issue is to add a strategy to the spy so that it returns a value:
spyOn(userService, 'apiUserSendPasswordResetLink').and.returnValue('whatever');

Related

Playwright How To Return page from a function that creates a browser context?

I am trying to make a function that opens the browser and logs into the page using basic auth. I would like the function to return the page so I can pick it up in the test and continue to use it.
When I pass browser to the function I need to create a new context inside the function so I can login with basic auth.
Creating a new browser context in the function works fine to open the page and login.
The problem is that I cannot return the new page from the function. The page that is returned from the function has no intellisense and fails when I attempt to use it in the normal way --- such as doing: page.locator('#id1').click() ---> test fails
// Custom Open Browser Login Function
export async function openBrowserAndLogin(browser, username, password){
const context = await browser.newContext({
httpCredentials:{
username: username,
password: password
},
})
const page = await context.newPage()
await page.goto('websiteurl.com')
return page
}
// Test
import { test } from '#playwright/test';
import {openBrowserAndLogin} from '../customfunctions.openBrowser.mjs'
test('login and do stuff', async ({ browser }) => {
const page = openBrowserAndLogin(browser,'user1', 'secretpassword')
page.locator('#account').click() // no methods exist on this page object???? Test Fail
})
So basically I am importing a function into the test. I pass the browser to the function. The function uses browser to create a new context, logs into application, and returns a new page.
The page returned from the function is dead.
Does anyone know a way to return a page from a context created inside an imported function?
Or if there is some other way to accomplish this that would be helpful too.
openBrowserAndLogin is not a good pattern. This abandons the reference to the browser context object so you can never close it, thereby leaking memory and potentially hanging the process (unless the test suite ungracefully terminates it for you).
Instead, prefer to let Playwright manage the page:
test('login and do stuff', async ({ page }) => {
// ^^^^
Now you can add credentials with Playwright's config or test.use:
import {test} from "#playwright/test"; // ^1.30.0
test.describe("with credentials", () => {
test.use({
httpCredentials: {
username: "user1",
password: "secretpassword"
}
});
test("login and do stuff", async ({page}) => {
await page.goto("https://example.com/");
await page.locator("#account").click();
});
});
Notice that I've awaited page.locator('#account').click().
Another option is to use fixtures.
You’re just missing await.
openBrowserAndLogin is an async function, so it’s returning a promise, which wouldn’t have the page methods itself. You need to unwrap it first, like so:
const page = await openBrowserAndLogin(browser,'user1', 'secretpassword')
That being said, I would definitely recommend doing the auth in global setup with storageState if you need the same login for every test and then just use the page fixture, or you could always override the page fixture or add your own or something similar. There are other potential ways too. But for what you have, just that small piece was missing.
Note that it’s also good practice to close your context if you manually create one.

How to properly test statements in setInterval() in Angular?

I have this very simple code:
public async authenticate(username: string, password: string) {
const authenticationResponse = await this.dataProvider.authenticate(username, password);
if (authenticationResponse.result.code == 0) {
//start interval for periodically checking authentication info
this._authInfoIntervalId = setInterval(() => {
this.getAuthInfo();
}, 2000);
In my unit tests only line with this.getAuthInfo() is not concidered as covered. Is it somehow possible to test this? I have tried some approaches with jasmine.createSpy but nothing seemed to work (most likely because I was doing it wrong). Can someone please help me to get it right? Thanks
UPDATE: I tried something like this
it('should test interval somehow', () => {
const intervalCallback = jasmine.createSpy("getAuthInfo");
jasmine.clock().install();
service.authenticate('username', 'password');
jasmine.clock().tick(2001);
expect(intervalCallback).toHaveBeenCalled();
})
and test fails instantly with AuthenticationService should test interval somehow FAILED Expected spy getAuthInfo to have been called.
SOLUTION: I had to spyOn also on dataProvider so I got right response to actually reach that part of code with interval
it('should test interval somehow', async () => {
const intervalCallback = spyOn(service, 'getAuthInfo');
spyOn(dataProvider, 'authenticate').and.returnValue(Promise.resolve(authenticateMockResponse));
jasmine.clock().install();
await service.authenticate('username', 'password');
jasmine.clock().tick(2001);
expect(intervalCallback).toHaveBeenCalled();
});
jasmine.createSpy() should be used for creating a bare spy object, see the documentation. It doesn't have any connection to your service, it's not what you are looking for.
You want to spy a function on an existing object (in your case the service), for which you can use the spyOn function.
it('should test interval somehow', () => {
const intervalCallback = spyOn(service, 'getAuthInfo');
jasmine.clock().install();
service.authenticate('username', 'password');
jasmine.clock().tick(2001);
expect(intervalCallback).toHaveBeenCalled();
})
In Angular, you can use the async and fakeAsync functions from the #angular/core/testing module to properly test statements within a setInterval() function.
First, you'll need to wrap your test in the fakeAsync function, which allows you to use the tick() function to advance the virtual time. Next, you'll need to wrap the code you want to test within the setInterval() function in an async function.

Why would Jasmine Spyon not be called

I have a test that is failing even though I am calling it. I am spying on the PrimeNG Message Service.
Below is some code that I have so far.
it('should throw error', () => {
const mockCall = spyOn(service, 'update$').and.throwError('Error');
const msgService = spyOn(messageService, 'add').and.callThrough();
expect(mockCall).toThrowError('Error');
expect(msgService).toHaveBeenCalled();
});
I am expecting this to pass this test since it gets called withing my update$ observable if there is an error. Here is the error
"Expected spy add to have been called"
As a rule of thumb you should not apply mocks to the method you are testing. You should be calling the method you are testing directly, then verify what happened. You should also avoid verifying your mocks. You've set up service.update$ to throw, that's what it will do, you do not need to verify it happened.
Your test should probably look something like this:
it('should throw error', () => {
// Set up
const mockCall = spyOn(service, 'update$').and.throwError('Error');
// Test
const message = {};
messageService.add(message);
// Verify
expect(mockCall).toHaveBeenCalledWith(message);
});

Jest testing with Node - Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout

I'm starting to test my code with Jest, and I can't make a seemingly simple test to pass. I am simply trying to check if what I receive from a Maogoose database request is an object.
The function fetchPosts() is working because I hooked it up with a React frontend and it is displaying the data correctly.
This is my function fetchPosts():
module.exports = {
fetchPosts() {
return new Promise((resolve, reject) => {
Posts.find({}).then(posts => {
if (posts) {
resolve(posts)
} else {
reject()
}
})
})
}
}
And my test:
it('should get a list of posts', function() {
return posts.fetchPosts().then(result => {
expect(typeof result).toBe('object')
})
})
This makes the test fail, and Jest says
'Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.'
QUESTION: How can I make this test pass?
You can expect asynchronous results using resolves, as shown in the Jest documentation.
In your case:
it('should get a list of posts', function() {
const result = posts.fetchPosts();
expect(result).resolves.toEqual(expect.any(Object));
})
…although I have a suspicion your list of posts is actually an array, so you probably want this:
it('should get a list of posts', function() {
const result = posts.fetchPosts();
expect(result).resolves.toEqual(expect.any(Array));
})
Another tip: You don't need to wrap the body of your fetchPost in an additional promise, you can simply return the promise you get from Posts.find and add a then to it, like this:
module.exports = {
fetchPosts() {
return Posts.find({}).then(posts => {
if (posts) {
return posts;
}
throw new Error('no posts'); // this will cause a promise rejection
})
}
}
It's also highly possible that you're not getting a response back from the DB at all from your test suite. Test suite's can call different environmental variables / configs that lead to different calls. This error can also be seen if no response is returned, as in - if someone blocks your IP from connecting, on and on.
Also if you are simply looking to increase the timeout, then you can do that by setting
jest.setTimeout(10000);
You can use this statement in beforeEach if you want to change the timeout for all your tests in that describe block or in the test/it/spec block if you want it for a single test.
For me none of the above worked so I tried older version of jest and it worked
npm i -D jest#25.2.7.
if you are using it with typescript make sure to degrade ts-jest as well
npm i -D jest#25.2.7 ts-jest#25.3.1

Spy method from dynamically obtained object

I use mongoose and I have login function, that tries to find user and then valid his password (I will not describe here all logic of this function, but only major parts to show you my problem).
function login(req, res) {
...
User.findOne(...)
.then((user) => {
user.validPassword(...);
...
});
}
I have defined a User model that contains a validPassword method and all work fine but I have trouble to spy validPassword method.
I use Jasmine to my tests and I tried to do this in this way:
const user = new User(...);
spyOn(user, 'validPassword').and.callThrough();
expect(user.validPassword).toHaveBeenCalled(); // was never called :(
And of course I called login function. When I test findOne method in this way, it works fine, but it is simpler because I call method from User constructor:
spyOn(User, 'findOne').and.callThrough();
expect(User.findOne).toHaveBeenCalled(); // this works fine!
I think my problem is related to different instances, because findOne method resolve a new user object for me and in the tests I create a second one, what is absolutely different object, but I'm not sure about this conjecture.
Can you tell me how to repair this?
Ok I fixed this.
1) I found solution of my problem here
2) I noticed that findOne method returns promise and then I do async task, so I had to check it with 'done' function before start testing.
function login(req, res) {
...
return User.findOne(...)
.then((user) => {
user.validPassword(...);
...
});
}
In jasmine:
beforeAll((done) => {
spyOn(User.prototype, 'validPassword').and.callThrough();
login(...).then(() => done());
}
it('calls validPassword', () => {
expect(User.prototype.validPassword).toHaveBeenCalled(); // works :)
});

Categories