I want to be able to assert whether my 'SIGN_IN' action works as intended.
My action looks like this (not the cleanest implementation but it's what I have to work with for now):
// actions.js
import {
SIGN_IN, SIGN_OUT, SET_AUTH_TOKEN, CLEAR_AUTH_TOKEN, USER_AUTHORISED,
} from '#/store/types';
import AuthService from '../../../authService';
export default {
async [SIGN_IN]({ commit, rootState }) {
const authService = new AuthService(rootState.clientSettings);
let response;
try {
response = await authService.getToken();
} catch (e) {
console.log('User not authorised to use application.');
}
if (response) {
commit(SET_AUTH_TOKEN, response);
} else {
commit(USER_AUTHORISED, false);
}
},
...
};
authService.getToken(); is what I want to mock the response of.
I want this to be able to resolve to a mock response in my test.
The class for AuthService looks like this:
// authService.js
import { PublicClientApplication } from '#azure/msal-browser';
class AuthService {
constructor(clientSettings) {
this.config = {
auth: {
clientId: clientSettings.clientId,
authority: `https://login.microsoftonline.com/${clientSettings.tenantId}`,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
};
this.scopes = [
clientSettings.appScope,
];
this.msal = new PublicClientApplication(this.config);
this.clientSettings = clientSettings;
}
...
async getToken() {
const { scopes } = this;
try {
const response = await this.msal.acquireTokenSilent({ scopes });
return response;
} catch (err) {
const response = await this.msal.acquireTokenPopup({ scopes });
return response;
}
}
...
}
export default AuthService;
This is what I tried in my test:
// actions.spec.js
import sinon, { mock } from 'sinon';
import actions from '#/store/modules/auth/actions';
import { SIGN_IN, SIGN_OUT } from '#/store/types';
import { PublicClientApplication } from '#azure/msal-browser';
import AuthService from '../../../../../src/authService';
import templateRootState from '../../../../helpers/rootState.json';
describe('Auth', () => {
describe('Actions', () => {
let state;
beforeEach(() => {
state = JSON.parse(JSON.stringify(templateRootState));
});
afterEach(() => {
sinon.reset();
});
describe('SIGN_IN', () => {
it.only('returns a token from the AuthService & sets it in the store', async () => {
const commit = sinon.spy();
const mockResponse = {
token: 'bobbins'
};
sinon.stub(AuthService, 'getToken').resolves(mockResponse);
const requestParams = { commit, rootState: state };
await actions[SIGN_IN](requestParams);
expect(commit.calledOnce).toEqual(true);
expect(commit.args.length).toEqual(1);
expect(commit.args[0][0]).toEqual('SET_AUTH_TOKEN');
expect(commit.args[0][1]).toEqual(mockResponse);
});
});
});
});
While debugging, I am unable to get getToken to be a stubbed response. It still calls the actual userService class instance created in my action: new AuthService(rootState.clientSettings);.
Hopefully you can see what I'm trying to achieve with my test. I just want to assert that SET_AUTH_TOKEN is triggered and nothing else. How can I stub out this AuthService class entirely so that my action uses that one instead? I don't really want to be passing an instance of AuthService into the action itself, there must be a neater way of doing it?
What you need is to stub prototype method.
sinon.stub(AuthService.prototype, 'getToken').resolves(mockResponse);
Reference: how to mock es6 class
Related
I have a simple composable useRoles which I need to test
import { computed } from "vue";
import { useStore } from "./store";
export default function useRoles() {
const store = useStore();
const isLearner = computed(() => store.state.profile.currentRole === "learner");
return {
isLearner
};
}
My approach of testing it is the following
import { afterEach, expect } from "vitest";
import useRoles from "./useRoles";
describe("useRoles", () => {
afterEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should verify values when is:Learner", () => { // works
vi.mock("./store", () => ({
useStore: () => ({
state: {
profile: {
currentRole: "learner"
},
},
}),
}));
const { isLearner } = useRoles();
expect(isLearner.value).toBeTruthy();
});
it("should verify values when is:!Learner", () => { //fails
vi.mock("./store", () => ({
useStore: () => ({
state: {
profile: {
currentRole: "admin"
},
},
}),
}));
const { isLearner } = useRoles(); // Values are from prev mock
expect(isLearner.value).toBeFalsy();
});
});
And useStore is just a simple function that I intended to mock
export function useStore() {
return {/**/};
}
The first test runs successfully, it has all the mock values I implemented but the problem is that it's not resetting for each test (not resetting at all). The second test has the old values from the previous mock.
I have used
vi.clearAllMocks();
vi.resetAllMocks();
but for some reason clear or reset is not happening.
How can I clear vi.mock value for each test?
Solution
As it turned out I should not be called vi.mock multiple times. That was the main mistake
Substitutes all imported modules from provided path with another module. You can use configured Vite aliases inside a path. The call to vi.mock is hoisted, so it doesn't matter where you call it. It will always be executed before all imports.
Vitest statically analyzes your files to hoist vi.mock. It means that you cannot use vi that was not imported directly from vitest package (for example, from some utility file)
Docs
My fixed solution is below.
import useRoles from "./useRoles";
import { useStore } from "./store"; // Required the mock to work
vi.mock("./store");
describe("useRoles", () => {
afterEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should verify values when is:Learner", () => {
// #ts-ignore it is a mocked instance so we can use any vitest methods
useStore.mockReturnValue({
state: {
profile: {
currentRole: "learner",
},
},
});
const { isLearner } = useRoles();
expect(isLearner.value).toBeTruthy();
});
it("should verify values when is:!Learner", () => {
// You need to use either #ts-ignore or typecast it
// as following (<MockedFunction<typeof useStore>>useStore)
// since original function has different type but vitest mock transformed it
(<MockedFunction<typeof useStore>>useStore).mockReturnValue({
state: {
profile: {
currentRole: "admin",
},
},
});
const { isLearner } = useRoles();
expect(isLearner.value).toBeFalsy();
});
});
vitest = v0.23.0
I ran into the same issue and was able to find a workaround. In your case it should look like this:
import { afterEach, expect } from "vitest";
import useRoles from "./useRoles";
vi.mock("./store");
describe("useRoles", () => {
it("should verify values when is:Learner", async () => {
const store = await import("./store");
store.useStore = await vi.fn().mockReturnValueOnce({
useStore: () => ({
state: {
profile: {
currentRole: "learner"
},
},
}),
});
const { isLearner } = useRoles();
expect(isLearner.value).toBeTruthy();
});
it("should verify values when is:!Learner", async () => {
const store = await import("./store");
store.useStore = await vi.fn().mockReturnValueOnce({
useStore: () => ({
state: {
profile: {
currentRole: "admin"
},
},
}),
});
const { isLearner } = useRoles(); // Value is from current mock
expect(isLearner.value).toBeFalsy();
});
});
I am not getting any clue how to mock a method. I have to write a unit test for this function:
index.ts
export async function getTenantExemptionNotes(platform: string) {
return Promise.all([(await getCosmosDbInstance()).getNotes(platform)])
.then(([notes]) => {
return notes;
})
.catch((error) => {
return Promise.reject(error);
});
}
api/CosmosDBAccess.ts
import { Container, CosmosClient, SqlQuerySpec } from "#azure/cosmos";
import { cosmosdbConfig } from "config/Config";
import { Workload } from "config/PlatformConfig";
import { fetchSecret } from "./FetchSecrets";
export class CosmoDbAccess {
private static instance: CosmoDbAccess;
private container: Container;
private constructor(client: CosmosClient) {
this.container = client
.database(cosmosdbConfig.database)
.container(cosmosdbConfig.container);
}
static async getInstance() {
if (!CosmoDbAccess.instance) {
try {
const connectionString = await fetchSecret(
"CosmosDbConnectionString"
);
const client: CosmosClient = new CosmosClient(connectionString);
// Deleting to avoid error: Refused to set unsafe header "user-agent"
delete client["clientContext"].globalEndpointManager.options
.defaultHeaders["User-Agent"];
CosmoDbAccess.instance = new CosmoDbAccess(client);
return CosmoDbAccess.instance;
} catch (error) {
// todo - send to app insights
}
}
return CosmoDbAccess.instance;
}
public async getAllNotesForLastSixMonths() {
const querySpec: SqlQuerySpec = {
// Getting data from past 6 months
query: `SELECT * FROM c
WHERE (udf.convertToDate(c["Date"]) > DateTimeAdd("MM", -6, GetCurrentDateTime()))
AND c.IsArchived != true
ORDER BY c.Date DESC`,
parameters: [],
};
const query = this.container.items.query(querySpec);
const response = await query.fetchAll();
return response.resources;
}
}
export const getCosmosDbInstance = async () => {
const cosmosdb = await CosmoDbAccess.getInstance();
return cosmosdb;
};
index.test.ts
describe("getExemptionNotes()", () => {
beforeEach(() => {
jest.resetAllMocks();
});
it("makes a network call to getKustoResponse which posts to axios and returns what axios returns", async () => {
const mockNotes = [
{
},
];
const cosmosDBInstance = jest
.spyOn(CosmoDbAccess, "getInstance")
.mockReturnValue(Promise.resolve(CosmoDbAccess.instance));
const kustoResponseSpy = jest
.spyOn(CosmoDbAccess.prototype, "getAllNotesForLastSixMonths")
.mockReturnValue(Promise.resolve([mockNotes]));
const actual = await getExemptionNotes();
expect(kustoResponseSpy).toHaveBeenCalledTimes(1);
expect(actual).toEqual(mockNotes);
});
});
I am not able to get instance of CosmosDB or spyOn just the getAllNotesForLastSixMonths method. Please help me code it or give hints. The complexity is because the class is singleton or the methods are static and private
This is rather a stylistic question. I'm using Pino in some of my Javascript/Typescript microservices. As they're running on AWS I'd like to propagate the RequestId.
When one of my functions is invoked, I'm creating a new child logger like this:
const parentLogger = pino(pinoDefaultConfig)
function createLogger(context) {
return parentLogger.child({
...context,
})
}
function createLoggerForAwsLambda(awsContext) {
const context = {
requestId: awsContext.awsRequestId,
}
return createLogger(context)
}
I'm then passing down the logger instance to all methods. That said, (... , logger) is in almost every method signature which is not too nice. Moreover, I need to provide a logger in my tests.
How do you do it? Is there a better way?
you should implement some sort of Dependency Injection and include your logger there.
if your using microservices and maybe write lambdas in a functional approach, you can handle it by separating the initialization responsibility in a fashion like this:
import { SomeAwsEvent } from 'aws-lambda';
import pino from 'pino';
const createLogger = (event: SomeAwsEvent) => {
return pino().child({
requestId: event.requestContext.requestId
})
}
const SomeUtil = (logger: pinno.Logger) => () => {
logger.info('SomeUtil: said "hi"');
}
const init(event: SomeAwsEvent) => {
const logger = createLogger(event);
someUtil = SomeUtil(logger);
return {
logger,
someUtil
}
}
export const handler = (event: SomeAwsEvent) => {
const { someUtil } = init(event);
someUtil();
...
}
The simplest way is to use some DI library helper to tackle this
import { createContainer } from "iti"
interface Logger {
info: (msg: string) => void
}
class ConsoleLogger implements Logger {
info(msg: string): void {
console.log("[Console]:", msg)
}
}
class PinoLogger implements Logger {
info(msg: string): void {
console.log("[Pino]:", msg)
}
}
interface UserData {
name: string
}
class AuthService {
async getUserData(): Promise<UserData> {
return { name: "Big Lebowski" }
}
}
class User {
constructor(private data: UserData) {}
name = () => this.data.name
}
class PaymentService {
constructor(private readonly logger: Logger, private readonly user: User) {}
sendMoney() {
this.logger.info(`Sending monery to the: ${this.user.name()} `)
return true
}
}
export async function runMyApp() {
const root = createContainer()
.add({
logger: () =>
process.env.NODE_ENV === "production"
? new PinoLogger()
: new ConsoleLogger(),
})
.add({ auth: new AuthService() })
.add((ctx) => ({
user: async () => new User(await ctx.auth.getUserData()),
}))
.add((ctx) => ({
paymentService: async () =>
new PaymentService(ctx.logger, await ctx.user),
}))
const ps = await root.items.paymentService
ps.sendMoney()
}
console.log(" ---- My App START \n\n")
runMyApp().then(() => {
console.log("\n\n ---- My App END")
})
it is easy to write tests too:
import { instance, mock, reset, resetCalls, verify, when } from "ts-mockito"
import { PaymentService } from "./payment-service"
import type { Logger } from "./logger"
const mockedLogger = mock<Logger>()
when(mockedLogger.info).thenReturn(() => null)
describe("Payment service: ", () => {
beforeEach(() => {
resetCalls(mockedLogger)
// reset(mockedLogger)
})
it("should call logger info when sending money", () => {
const paymentService = new PaymentService(instance(mockedLogger))
expect(paymentService.sendMoney()).toBe(true)
})
})
I would not use the requestId as part of the context of the logger, but use it as the payload of the logger, like logger.info({ requestId }, myLogMessage). This was you can have a simple function create a child logger that you can use for the entire module.
im having this method in my component that makes an API call with axios, I checked the docs on how to test it but I cant seem to figure out how to do so. Any help would be appreciated.
loadContents() {
axios.get('/vue_api/content/' + this.slug).then(response => {
this.page_data = response.data.merchandising_page
}).catch(error => {
console.log(error)
})
},
You could use moxios or axios-mock-adapter to automatically mock Axios requests. I prefer the latter for developer ergonomics.
Consider this UserList component that uses Axios to fetch user data:
// UserList.vue
export default {
data() {
return {
users: []
};
},
methods: {
async loadUsers() {
const { data } = await axios.get("https://api/users");
this.users = data;
}
}
};
With axios-mock-adapter, the related test stubs the Axios GET requests to the API URL, returning mock data instead:
import axios from "axios";
const MockAdapter = require("axios-mock-adapter");
const mock = new MockAdapter(axios);
import { shallowMount } from "#vue/test-utils";
import UserList from "#/components/UserList";
describe("UserList", () => {
afterAll(() => mock.restore());
beforeEach(() => mock.reset());
it("loads users", async () => {
mock
.onGet("https://api/users")
.reply(200, [{ name: "foo" }, { name: "bar" }, { name: "baz" }]);
const wrapper = shallowMount(UserList);
await wrapper.vm.loadUsers();
const listItems = wrapper.findAll("li");
expect(listItems).toHaveLength(3);
});
});
demo
Below is my code, I want login() and authenticated() functions to wait for getProfile() function to finish its execution. I tried several ways like promise etc. but I couldn't implement it. Please suggest me the solution.
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
// Avoid name not found warnings
declare var Auth0Lock: any;
#Injectable()
export class Auth {
// Configure Auth0
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, {
additionalSignUpFields: [{
name: "address", // required
placeholder: "enter your address", // required
icon: "https://example.com/address_icon.png", // optional
validator: function(value) { // optional
// only accept addresses with more than 10 chars
return value.length > 10;
}
}]
});
//Store profile object in auth class
userProfile: any;
constructor() {
this.getProfile(); //I want here this function to finish its work
}
getProfile() {
// Set userProfile attribute if already saved profile
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on("authenticated", (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
// Fetch profile information
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
// Handle error
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
});
});
};
public login() {
this.lock.show();
this.getProfile(); //I want here this function to finish its work
};
public authenticated() {
this.getProfile(); //I want here this function to finish its work
return tokenNotExpired();
};
public logout() {
// Remove token and profile from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
};
}
Like you saw in the comments, you have to use Promise or Observable to achieve this, since your behaviour is pretty simple, you should use Promise because Observable will have a lot of features you don't need in this case.
Here is the Promise version of your service:
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
// Avoid name not found warnings
declare var Auth0Lock: any;
#Injectable()
export class Auth {
// Configure Auth0
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, {
additionalSignUpFields: [{
name: "address", // required
placeholder: "enter your address", // required
icon: "https://example.com/address_icon.png", // optional
validator: function(value) { // optional
// only accept addresses with more than 10 chars
return value.length > 10;
}
}]
});
//Store profile object in auth class
userProfile: any;
constructor() {
this.getProfile(); //I want here this function to finish its work
}
getProfile():Promise<void> {
return new Promise<void>(resolve => {
// Set userProfile attribute if already saved profile
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on("authenticated", (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
// Fetch profile information
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
// Handle error
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
resolve()
});
});
})
};
public login(): Promise<void>{
this.lock.show();
return this.getProfile(); //I want here this function to finish its work
};
public authenticated():void{
this.getProfile().then( () => {
return tokenNotExpired();
});
};
public logout():void {
// Remove token and profile from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
};
}
More on Promise here
I would recommend that you set up getProfile to return an observable. Then your other functions can subscribe to that function and do their actions in the subscribe function. The Angular 2 HTTP tutorial gives an example of this