I have a really weird situation where my Jest tests are passing in my Windows 10 desktop and Macbook Pro, but they are not passing in 2 of my other friends' Windows 10 desktops.
Code that is being tested
import { addTerminalItem } from '../../store'
...
class LoginUser extends EventHandler {
...
async handle () {
if (this.isFromOauthRedirect) {
try {
await this._handleOauthRedirect()
} catch (e) {
addTerminalItem(new ErrorMessage(e.message))
}
return
}
if (await zaClient.isUserLoggedIn('testUserId')) {
// TODO: user is already logged in, do something
} else {
const loginStartSecret = uuidv4()
localStorage.setItem(LOGIN_START_SECRET, loginStartSecret)
addTerminalItem(new LoginMessage(loginStartSecret))
}
}
...
}
export const loginUser = new LoginUser()
The testing code does the following:
Adds invalid LOGIN_START_SECRET so that actual code throws exception entering the first catch.
Subscribes the event handler to the event WELCOME_MESSAGE_RENDERED.
Mocks the store.addTerminalItem module function.
Publishes the event so the above async handle() function is triggered.
Checks that the mocked function is called.
import * as store from '../../../store'
...
test('different login start secret in localstorage', async () => {
localStorage.setItem(LOGIN_START_SECRET, 'different-secret')
zaClient.login = jest.fn(() => true)
store.addTerminalItem = jest.fn()
await pubsub.publish(WELCOME_MESSAGE_RENDERED)
expect(store.addTerminalItem).toHaveBeenCalledWith(expect.any(ErrorMessage))
const errorMessage = store.addTerminalItem.mock.calls[0][0]
expect(errorMessage.message).toBe(loginSecurityErrorMsg)
})
As I said on my computer it shows correctly that addTerminalItem function is called once with the correct argument on both machines I have at home. However this mocked function is never called and fails on 2 of my friends' machines. The actual error message they get is below:
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: Any<ErrorMessage>
Number of calls: 0
Here are the following things we tried so far:
Fresh git clone, yarn install, and yarn test. I pass and they don't.
With addTerminalItem mocked, we added a console.log inside addTerminalItem and it correctly doesn't log, but still 0 number of calls.
With addTerminalItem spyed, we added a console.log inside addTerminalItem and it correctly logs, but still 0 number of calls (this makes no sense to me)
We matched our yarn version.
We carefully debug stepped through the code to make sure all other things were working as expected.
If anyone could give us any pointers here it would be greatly appreciated.
Hard to be definitive without the code at hand, but try using jest.mock:
import {addTerminalItem} from "../../../store";
jest.mock('../../../store', () => ({
addTerminalItem: jest.fn()
));
//... stuff ...
test('different login start secret in localstorage', async () => {
localStorage.setItem(LOGIN_START_SECRET, 'different-secret')
zaClient.login = jest.fn(() => true)
await pubsub.publish(WELCOME_MESSAGE_RENDERED)
expect(addTerminalItem).toHaveBeenCalledWith(expect.any(ErrorMessage))
const errorMessage = addTerminalItem.mock.calls[0][0]
expect(errorMessage.message).toBe(loginSecurityErrorMsg)
})
Related
I am developing a video chat app using react-native-voximplant, everything works, but when the app loads first time, the login functionality is happening inside the useEffect. So, the problem is, when my HomeScreen mounts, the useEffects' should fire and I should get logged in voximplant sdk and now if I tap on call icon, the following is shown in the console:
When HomeScreen mounts:
LOG Client: emit: no handlers for event: AuthResult
LOG Error while trying to login to voximplant: {"code": 491, "name": "AuthResult", "result": false}
LOG Client: emit: no handlers for event: ConnectionEstablished
Now when I tap on call to make a call:
// after a second...
WARN Possible Unhandled Promise Rejection (id: 0):
"NOT_LOGGED_IN"
Now when I hit save (ctrl+s), I get logged in and now if I make a call, it works! I don't know what I am doing wrong here. Is something wrong with useEffect or I'm doing something wrong?
The HomeScreen code:
import {Voximplant} from 'react-native-voximplant';
const HomeScreen = () => {
const voximplant = Voximplant.getInstance();
useEffect(() => {
const signInToVoximplant = async () => {
try {
const fqUsername = '...'; // credentials are correct, don't worry about them.
const password = '...';
await voximplant.login(fqUsername, password);
} catch (error) {
console.log('Error while trying to login to voximplant: ', error);
}
};
signInToVoximplant();
}, []);
useEffect(() => {
const connect = async () => {
const status = await voximplant.getClientState();
if (status === Voximplant.ClientState.DISCONNECTED) {
await voximplant.connect();
} else if (status === Voximplant.ClientState.LOGGED_IN) {
console.log('[INFO] VOXIMPLANT CONNECTED');
return;
}
};
connect();
}, []);
}
return ...
The call will only work if I hit ctrl+s i.e. save the file, otherwise it will throw Unhandled Promise rejection... because it is not logged in, however I have written/provided the login inside the useEffect.
According to your logs, when the application starts, login is failed with 491 error that means that the client is in invalid state. It happens because you invoke login API before the connection the Voximplant Cloud is established (connect API).
When the app is reloaded, native code is not reinitialised (but js does) and actually the client is still connected to the Voximplant Cloud, that's why login is successful the second time.
To fix the issue, you need to change the order of Voximplant SDK API invocation: first call Client.connect API and then Client.login.
It is also better to move connect/login to the a single useEffect.
Both useEffect is run on mounted, but ¿whichone first?.
Try put both functions in the same useEffect, and call them in order:
useEffect(() => {
const signInToVoximplant = async () => {
...
};
const connect = async () => {
...
};
signInToVoximplant();
connect();
}, []);
I am writing a typeScript program which hits an external API. In the process of writing tests for this program, I have been unable to correctly mock-out the dependency on the external API in a way that allows me to inspect the values passed to the API itself.
A simplified version of my code that hits the API is as follows:
const api = require("api-name")();
export class DataManager {
setup_api = async () => {
const email = "email#website.ext";
const password = "password";
try {
return api.login(email, password);
} catch (err) {
throw new Error("Failure to log in: " + err);
}
};
My test logic is as follows:
jest.mock("api-name", () => () => {
return {
login: jest.fn().mockImplementation(() => {
return "200 - OK. Log in successful.";
}),
};
});
import { DataManager } from "../../core/dataManager";
const api = require("api-name")();
describe("DataManager.setup_api", () => {
it("should login to API with correct parameters", async () => {
//Arrange
let manager: DataManager = new DataManager();
//Act
const result = await manager.setup_api();
//Assert
expect(result).toEqual("200 - OK. Log in successful.");
expect(api.login).toHaveBeenCalledTimes(1);
});
});
What I find perplexing is that the test assertion which fails is only expect(api.login).toHaveBeenCalledTimes(1). Which means the API is being mocked, but I don't have access to the original mock. I think this is because the opening line of my test logic is replacing login with a NEW jest.fn() when called. Whether or not that's true, I don't know how to prevent it or to get access to the mock function-which I want to do because I am more concerned with the function being called with the correct values than it returning something specific.
I think my difficulty in mocking this library has to do with the way it's imported: const api = require("api-name")(); where I have to include an opening and closing parenthesis after the require statement. But I don't entirely know what that means, or what the implications of it are re:testing.
I came across an answer in this issue thread for ts-jest. Apparently, ts-jest does NOT "hoist" variables which follow the naming pattern mock*, as regular jest does. As a result, when you try to instantiate a named mock variable before using the factory parameter for jest.mock(), you get an error that you cannot access the mock variable before initialization.
Per the previously mentioned thread, the jest.doMock() method works in the same way as jest.mock(), save for the fact that it is not "hoisted" to the top of the file. Thus, you can create variables prior to mocking out the library.
Thus, a working solution is as follows:
const mockLogin = jest.fn().mockImplementation(() => {
return "Mock Login Method Called";
});
jest.doMock("api-name", () => () => {
return {
login: mockLogin,
};
});
import { DataManager } from "../../core/dataManager";
describe("DataManager.setup_api", () => {
it("should login to API with correct parameters", async () => {
//Arrange
let manager: DataManager = new DataManager();
//Act
const result = await manager.setup_api();
//Assert
expect(result).toEqual("Mock Login Method Called");
expect(mockLogin).toHaveBeenCalledWith("email#website.ext", "password");
});
});
Again, this is really only relevant when using ts-jest, as using babel to transform your jest typescript tests WILL support the correct hoisting behavior. This is subject to change in the future, with updates to ts-jest, but the jest.doMock() workaround seems good enough for the time being.
I have an app that uses graphql subscriptions for chat functionality. I have managed to successfully get the subscription working however after introducing the withFilter function in order to filter which clients the messages get sent to I am getting the following error on the frontend:
Subscription field must return Async Iterable. Received: undefined
Here is my subscription resolver:
const { PubSub, withFilter } = require('graphql-yoga');
const pubsub = new PubSub();
pubsub.ee.setMaxListeners(30);
const Subscription = {
detailedConversation: withFilter(
() => pubsub.asyncIterator('detailedConversation'),
(payload, args) => {
return true;
}
)
};
module.exports = {
Subscription,
pubsub
};
As the second parameter of withFilter has to be a function that returns a boolean, I have just set this to return true for the time being.
Graphql-yoga uses graphql-subscriptions under the hood and after reading the documentation on implementation here I can't see what i'm doing wrong?
FYI the error occurs when attempting to subscribe for the first time to a conversation, not whilst sending a message or anything
I know this question is old but gonna give my solution, to others that might come looking for exactly the same solution...
First thing to note is that I'm using the graphql-redis-subscriptions implementation instead of the default implementation.
userUpdated: {
subscribe: withFilter((_, args, { pubsub }) => pubsub.asyncIterator('userUpdated'), (payload, vars) => vars.usersId.includes(payload.userUpdated.id))
}
documentation link
you just check the two arguments are the same then return your actions.
UserUpdated:{
withFilter(
() => pubsub.asyncIterator('UserUpdated'),
(payload, variables) => {
return (payload.UserUpdated.id === variables.channelid);
},
),
}
I have a simple function like this, I would like to mock the return value of authentication.getAccessToken() with a valid accessToken, I am having hard time doing this. Tried different ways but couldn't succeed, Can someone help me on this?
import decodeJWT from "jwt-decode";
import authentication from "#kdpw/msal-b2c-react";
export const testfunction = () => {
const jwt = decodeJWT(authentication.getAccessToken());
var current_time = Date.now() / 1000;
var remaining_time = jwt.exp - current_time;
return "testing done"
}
Following is the unit test which I have been trying, As authentication.getAccessToken() doesn't get any value it is throwing InvalidToken error.
import * as commonUtil from "../commonUtil";
describe('test test function', () => {
it("Mock", () => {
//The following runs but test fails due to null return
const authentication = require("#kdpw/msal-b2c-react");
authentication.getAccessToken = jest.fn().mockReturnValue(false);
expect(commonUtil.testfunction()).toBe(false)
});
});
Error message
Comparing two different types of values. Expected boolean but received null.
You need to import authentication within your test.
See CodeSandbox example. Open with editor to check out the unit tests.
In short, you need to do something like this.
test('test test function', () => {
const resp = { data: '' };
import authentication from "#kdpw/msal-b2c-react";
authentication.getAccessToken = jest.fn().mockReturnValue("eyJ0eXAiOiJKV1QiLCJhbdjhkbE5QNC1jNTdkTzZR...");
expect(commonUtil.testfunction()).toEqual("testing done")
});
Use describe to wrap test cases that uses mocked authentication so the mocked function will only stay local in that specific describe and everything outside it will use the real authentication.getAccessToken().
Problem
I'm testing a custom redux middleware using Jest and SinonJS and more precisely I want to test if some functions are called on special conditions inside the middleware.
I use SinonJS for creating the spies and I run my tests with Jest. I initialised the spies for the specific functions I want to track and when I check if the spies has been called, the spies has not been even if it should be (manually tested).
Code
Here is the middleware I want to test :
import { Cookies } from 'react-cookie';
import setAuthorizationToken from './setAuthorizationToken';
let cookies = new Cookies();
export const bindTokenWithApp = (store) => (next) => (action) => {
// Select the token before action
const previousToken = getToken(store.getState());
// Dispatch action
const result = next(action);
// Select the token after dispatched action
const nextToken = getToken(store.getState());
if (previousToken !== nextToken) {
if (nextToken === '') {
setAuthorizationToken(false);
cookies.remove(SESSION_COOKIE_NAME, COOKIE_OPTIONS);
} else {
cookies.set(SESSION_COOKIE_NAME, nextToken, COOKIE_OPTIONS);
setAuthorizationToken(nextToken);
}
}
return result;
};
Here is my actual test
import { bindTokenWithApp } from './middleware';
import { Cookies } from 'react-cookie';
import sinon, { assert } from 'sinon';
import setAuthorizationToken from './setAuthorizationToken';
describe('bindTokenWithApp', () => {
const next = jest.fn();
const action = jest.fn();
let cookies = new Cookies();
it('removes cookies when there is no token', () => {
// My actual not working spies
const cookieSpy = sinon.spy(cookies.remove);
const authSpy = sinon.spy(setAuthorizationToken);
// Stub for the specific case. This code works,
// I console.logged in the middleware and I'm getting the below values
const getState = sinon.stub();
getState.onFirstCall().returns({ auth: { token: 'a token' } });
getState.onSecondCall().returns({ auth: { token: '' } });
const store = { getState: getState };
bindTokenWithApp(store)(next)(action);
assert.calledOnce(cookieSpy);
assert.calledOnce(authSpy);
// Output : AssertError: expected remove to be called once but was called 0 times
// AssertError: expected setAuthorizationToken to be called once but was called 0 times
cookieSpy.restore(); // <= This one works
authSpy.restore(); // TypeError: authSpy.restore is not a function
});
});
I've read SinonJS doc and a few StackOverFlow posts but without solutions. I also can't call authSpy.restore();. I think I do not initialise spies the right way and I'm misunderstanding a concept in SinonJS but I can't find which one !
The setAuthorizationToken signature is
(alias) const setAuthorizationToken: (token: any) => void
import setAuthorizationToken
I think it's a classical module so I can't figure out why I struggle with authSpy.restore();
The two spies you have actually have two different fixes, both with the same underlying problem. sinon.spy(someFunction) doesn't actually wrap someFunction itself, it returns a spy for it but doesn't perform any replacement.
For the first spy, there exists a shorthand to automatically wrap an object method: sinon.spy(cookie, 'remove') should do what you need.
For the second spy, it is more complicated as you need to wrap the spy around the default export of setAuthorizationToken. For that you will need something like proxyquire. Proxyquire is a specialized require mechanism that allows you to replace imports with your desired test methods. Here's a brief of what you'll need to do:
const authSpy = sinon.spy(setAuthorizationToken);
bindTokenWithApp = proxyquire('./middleware', { './setAuthorizationToken': authSpy});