I want to create a JavaScript Github action and use Jest for testing purposes. Based on the docs I started parsing the input, given the following example code
import { getInput } from '#actions/core';
const myActionInput = getInput('my-key', { required: true });
Running this code during development throws the following error
Input required and not supplied: my-key
as expected because the code is not running inside a Github action environment. But is it possible to create tests for that? E.g.
describe('getMyKey', () => {
it('throws if the input is not present.', () => {
expect(() => getMyKey()).toThrow();
});
});
How can I "fake" / mock such an environment with a context to ensure my code works as expected?
There are several approaches you can take.
Set Inputs Manually
Inputs are passed to actions as environment variables with the prefix INPUT_ and uppercased. Knowing this, you can just set the respective environment variable before running the test.
In your case, the input my-key would need to be present as the environment variable named INPUT_MY-KEY.
This should make your code work:
describe('getMyKey', () => {
it('throws if the input is not present.', () => {
process.env['INPUT_MY-KEY'] = 'my-value';
expect(() => getMyKey()).toThrow();
});
});
Use Jest's Mocking
You could use jest.mock or jest.spyOn and thereby mock the behaviour of getInput.
Docs: ES6 Class Mocks
Abstract Action
I don't like setting global environment variables, because one test might affect another depending on the order they are run in.
Also, I don't like mocking using jest.mock, because it feels like a lot of magic and I usually spend too much time getting it to do what I want. Issues are difficult to diagnose.
What seems to bring all the benefits with a tiny bit more code is to split off the action into a function that can be called by passing in the "global" objects like core.
// index.js
import core from '#actions/core';
action(core);
// action.js
function action(core) {
const myActionInput = core.getInput('my-key', { required: true });
}
This allows you to nicely test your action like so:
// action.js
describe('getMyKey', () => {
it('gets required key from input', () => {
const core = {
getInput: jest.fn().mockReturnValueOnce('my-value')
};
action(core);
expect(core.getInput).toHaveBeenCalledWith('my-key', { required: true });
});
});
Now you could say that we're no longer testing if the action throws an error if the input is not present, but also consider what you're really testing there: You're testing if the core action throws an error if the input is missing. In my opinion, this is not your own code and therefore worthy of testing. All you want to make sure is that you're calling the getInput function correctly according to the contract (i.e. docs).
Related
I am doing a project using Nodejs for the backend, vanilla JS compiled with Parcel Bundler for the client side JS and PUG template engine to generate the views.
Note that I also use the FullCalendar v5 plugin. I don't think this is relevant as I feel this situation could happen without it, but still, it is the reason I encounter this problem at the moment.
Let's get straight to the point : I have a "main" parent function called initCalendar().
It initializes the calendar, creates the FullCalendar instance (along with all the calendars methods), sets it up depending on the configs given and renders it on the view. This function is the top level one of events.js, the "main" function as I like to call it.
initCalendar() is exported from events.js using the export keyword : export const initCalendar = () => { … }.
After this, I coded the calendar proprietary functions, which allow me to perform whatever action I want based on the ones done on the calendar. Like eventClick() for example, which executes whenever an event from the calendar is clicked, as its name suggests.
The point is, I created some functions in this eventClick() function (which itself is in initCalendar()), some of which I need to use in index.js. Therefore the need to export them. Also, I can't move these functions outside of initCalendar() scope, as I will loose important variables needed for my functions to run properly, and I would like to avoid using global variables.
My custom functions are nested like so : initCalendar() -> eventClick() -> myFunction() ("main" exported parent function -> intermediate calendar function -> my functions (to be exported)).
In case you're wondering why I have to do it this way, it is to keep the same workflow I have been using so far for all the client side JS of the project, trying to do it "the Parcel way". I have lots of exported functions that are imported in index.js from many different files, but this problem only got here when I included FullCalendar to the mix.
So the solution I found for now is to export my functions directly from eventClick(), using the exports keyword this time : exports.myFunction = myFunction. Doing this, I can then import them in index.js and continue to use the same workflow I used for all the client side JS (remember, compiled with Parcel Bundler).
What do you think about this "technique" ? Isn't it bad practice to export a child function from an already exported parent function ?
It seems quite hacky to me and I don't really like that… But I didn't find any better solution yet. Maybe someone could give me some insight on wether or not it is OK to do so and if not, how to solve the problem another way ? I thought maybe using callback functions, but I can not get it to work this way.
------- EDIT : Some code -------
Here is some code. I tried to cut it to the minimum, because the code of the clickEvent() function is literally hundred of lines long, and the one for FullCalendar is even bigger.
events.js : As you can see, the eventClick() function first opens a Modal which contains all the event info (that I didn't write because not relevant) and one button to delete the clicked event.
This is this button that should have his listener set from index.js calling the "exported child function" removeEvent() on a click event, to delete the associated event from DB and calendar.
There is other functions in the same style in there but this one should be enough to see what I'm talking about.
// events.js
// … All the es6 imports : { Calendar } - { Modal } - axios; etc …
// As you can see, if I try to export the removeEvent() function from here,
// it would work as exporting goes but I won't have the Modal instance of
// `eventInfoModal` used in `.then()`. Same thing with `const calendar`,
// because they would not be in the scope of `initCalendar()`.
// Therefore I won't be able to call the functions I do on them in `.then()`
export const initCalendar = () => {
const calendarEl = document.getElementById('calendar');
const calendar = new Calendar(calendarEl, {
// … Ton of code to config FullCalendar, import the events, other calendar functions etc…
eventClick: function(eventInfo) {
const eventId = eventInfo.event.id;
const infoModalContainer = document.getElementById('event-info-modal');
// Modal created from an imported class
const eventInfoModal = new Modal(
infoModalContainer,
this.el
);
eventInfoModal.init();
// … Lots of code to populate the modal with the event data, buttons etc …
// So the point here is to call this function from index.js (code of index.js below)
function removeEvent() {
if (confirm('Êtes-vous sûr de vouloir supprimer ce RDV ?')) {
deleteEvent(eventId)
.then(() => {
eventInfoModal.close();
calendar.refetchEvents();
});
}
}
// The "exported child function" I was talking about
exports.removeEvent = removeEvent;
// Then other functions defined and exported the same way, to be used just like removeEvent() in index.js
});
calendar.render();
// Function called from removeEvent() in eventClick() above, just doing an axios DELETE request, no need to write it
async function deleteEvent(eventId) {
}
};
index.js : Here I import all the exported functions from the other files (only showing the one we are talking about obviously) and try to group and set my listeners together by view or "by category", listeners that will then call the corresponding functions imported from the other files, to execute the needed actions.
// index.js
// … All the es6 imports, including :
import { removeEvent } from './events';
const userEventsPage = document.getElementById('user-events');
if (userEventsPage) {
const deleteEventBtn = document.getElementById('delete-event');
userEventsPage.addEventListener('click', evt => {
if (evt.target === deleteEventBtn) {
removeEvent();
}
});
}
Thank you very much
Posting my comment as an answer, as I believe that's the right way to solve this.
You should add a click handler to the delete-event button when you create the modal.
Also, from the look of the code shared, your Modal should have like an onRemoveButtonClicked property, that should be assigned to the removeEvent function that you're writing right now. I can't see why you need to export it.
I am totally confused. There are many sources out there contradicting each other about the definitions of Dependency Injection and Inversion of Control. Here is the gist of my understanding without many additional detail, that in most cases made things more convoluted for me: Dependency Injection means that instead of my function conjuring the required dependencies, it is given the dependency as a parameter. Inversion of Control means that, for instance when you use a framework it is the framework that calls your userland code, and the control is inversed because in the 'default' case your code would be calling specific implementations in a library.
Now as I understand, somehow along the way, because my function that doesn't conjure up the dependencies anymore but gets it as an argument Inversion of Control magically happens when I use dependency injection like below.
So here is a silly example I wrote for myself to wrap my head around the idea:
getTime.js
function getTime(hour) {
return `${hour} UTC`
}
module.exports.getTime = getTime
welcome.js
function welcomeUser(name, hour) {
const getTime = require('./time').getTime
const time = getTime(`${hour} pm`)
return(`Hello ${name}, the time is ${time}!`)
}
const result = welcomeUser('Joe', '11:00')
console.log(result)
module.exports.welcomeUser = welcomeUser
welcome.test.js
const expect = require('chai').expect
const welcomeUser = require('./welcome').welcomeUser
describe('Welcome', () => {
it('Should welcome user', () => {
// But we would want to test how the welcomeUser calls the getTime function
expect(welcomeUser('Joe', '10:00')).to.equal('Hello Joe, the time is 10:00 pm UTC!')
})
})
The problem now is that the call of the getTime function is implemented in the welcome.js function, and it can not be intercepted by a test. What we would like to do is to test how the getTime function is called, and we can't to that this way.
The other problem is that the getTime function is pretty much harcoded, so we can't mock it, and that could be useful because we only want to test the welcomUser function separately, as that is the use of a unit test (the getTime function could be simultaneously implemented, for instance).
So the main problem is that the code is tightly coupled, it's harder to test and it is just wreaking havoc all around the place. Now let's use dependency injection:
getTime.js
function getTime(hour) {
return `${hour} UTC`
}
module.exports.getTime = getTime
welcome.js
const getTime = require('./time').getTime
function welcomeUser(name, hour, dependency) {
const time = dependency(hour)
return(`Hello ${name}, the time is ${time}!`)
}
const result = welcomeUser('Joe', '10:00', getTime)
console.log(result)
module.exports.welcomeUser = welcomeUser
welcome.test.js
const expect = require('chai').expect
const welcomeUser = require('./welcome').welcomeUser
describe('welcomeUser', () => {
it('should call getTime with the right hour value', () => {
const fakeGetTime = function(hour) {
expect(hour).to.equal('10:00')
}
// 'Joe' as an argument isn't even neccessary, but it's nice to leave it there
welcomeUser('Joe', '10:00', fakeGetTime)
})
it('should log the current message to the user', () => {
// Let's stub the getTime function
const fakeGetTime = function(hour) {
return `${hour} pm UTC`
}
expect(welcomeUser('Joe', '10:00', fakeGetTime)).to.equal('Hello Joe, the time is 10:00 pm UTC!')
})
})
As I understood, what I did above was Dependency Injection. Multiple sources claim that Dependency Injection is not possible without Inversion of Control. But where does Inversion of Control come into the picture?
Also what about the regular JavaScript workflow, where you just import the dependencies globally and use them later in your functions, instead of require-ing them inside of the functions or giving it to them as parameters?
Check Martin Fowler's article on IoC and DI. https://martinfowler.com/articles/injection.html
IoC: Very generic word. This inversion can happen in many ways.
DI: Can be viewed as one branch of this generic word IoC.
So in your code when you specifically implements DI, one would say your code has this general idea of IoC in the flavor of DI. What really inversed is, the default way of looking for behavior (default way is writing it within the method, inversed way would be getting behavior injected from outside).
I have a network module to ping a legacy database with data in multiple formats, and I want to standardize it here in the network module before passing it into the application so my application can expect a certain format of data (don't want the old, poor formatting polluting my business logic). I'm struggling with how to pass mock data through as this network module, specifically as it relates to the formatter. Here's what I mean:
// User API Network Module
// UserAPI.ts
export const getUser = (uid: String, callback: (GetUserResponse) => void): void => {
// Do network call here and format the data into a typescript type
// matching the GetUserResponse structure by business logic expects
callback(formattedData)
}
In my test file, I can mock this call easily with:
import { getUser } from "./UserAPI"
jest.mock("./UserAPI", () => ({
getUser: (uid: String, callback: (GetUserResponse) => void) => {
const mockedUserData = require("./mockUser.json")
// But how do I format it here?
return formattedMockedUserData
},
}))
I can create a formatter function in my UserAPI.ts file, export it, and run it in the jest mock, but I'm wondering if that's a best practice because it technically leaks the UserAPI implementation details. And I point that out only because no other file cares about how UserAPI formats things except UserAPI. If I have to leak it for testing purposes, I'll do that. But is there a better way to mock the network call and run it through a formatter without exposing additional implementation details?
And please be gentle on my typescript - I come from both a JS and strongly typed background, but this is my first venture into using typescript :)
Even though it's not used multiple places extract it - following Single Responsibility Principle - into its own construct. You test all formatting logic in Formmater Test not in User API Test. Additionally you can test the integration of Formatter with User API in an Integration Test.
Within an ExpresS API I'm using the Postmark library to send an email, which is initiated like this:
var postmark = require("postmark");
var client = new postmark.Client("aaaa-bbbbb-cccc");
And then used to send a password reset mail later on with:
client.sendEmailWithTemplate(
// Options here
);
Now, I would like to test this function has been called, but I have difficulties finding out how to mock/spy on this.
I have tried the following (simplified):
const request = require("supertest");
const app = require("../server/app");
const postmark = require("postmark");
jest.mock("postmark");
describe("API Tests", () => {
test("it should give a reset link when requesting with existing e-mail address", () => {
return request(app)
.post("/api/auth/passwordreset")
.send({
email: "user1#test.test"
})
.then(response => {
expect(postmark.Client).toHaveBeenCalled();
});
});
});
This works, but it's only testing if postmark has been used, since I can't figure out how to actually test the client.sendEmailWithTemplate method
Any suggestions on how to accomplish this?
EDIT: following up on #samanime answer I created a repo to illustrate the 'challenge'
https://github.com/Hyra/jest_test_example
You can specifically mock out the Client function that is returned by the mocked postmark to return an object with mocked functions.
In Jest, you can provide specific mocking code for a node_modules by creating a folder named __mocks__ at the same level as node_modules, i.e.,
/project-root
/node_modules
/__mocks__
Note, that is two underscores on each side.
In there, make a function named <package_name>.js (in your case, postmark.js). It will then load whatever is exported by that when you use the mock.
In that file, you can mock it out as needed. Something like this would probably work:
// global jest
module.exports = {
Client: jest.fn(() => ({
sendEmailWithTemplate: jest.fn(() => {})
}))
};
It doesn't have to be as compact as this, but basically it makes postmark have a function called Client which returns an object that has a functiono called sendEmailWithTemplate, both of which are mocks/spys.
Then you can just check if postmark.Client.sendEmailWithTemplate was called.
The one gotcha is you'll need to be sure to reset all of these in between tests. You could do this manually in your beforeEach(), but if you are going to reuse it, I like to add an extra function named __reset() which will reset the code and just call that:
// global jest
const mockedPostmark = {
Client: jest.fn(() => ({
sendEmailWithTemplate: jest.fn(() => {})
}))
};
mockedPostmark.__reset = () => {
mockedPostmark.Client.mockClear();
mockedPostmark.Client.sendEmailWithTemplate.mockClear();
};
module.exports = mockedPostmark;
You can add additional functions as needed as well.
In a Meteor app, I need to test some client code that has statements such as
Meteor.call('foo', param1, param2, (error, result) => { .... });
And, in these methods, I have security checks to make sure that the method can only be called by authenticated users. However, all these tests fail during tests because no user is authenticated.
In each server methods, I check users like this
if (!Roles.userIsInRole(this.userId, [ ...roles ], group)) {
throw new Meteor.Error('restricted', 'Access denied');
}
I have read that we should directly export the server methods and test them directly, and I actually do this for server methods testing, but it is not possible, here, since I need to test client code that depend on Meteor.call.
I also would certainly not want to have if (Meteor.isTest || Meteor.isAppTest) { ... } all over the place....
I thought perhaps wrapping my exported methods like this :
export default function methodsWrapper(methods) {
Object.keys(methods).forEach(method => {
const fn = methods[method];
methods[method] = (...args) => {
const user = Factory.create('user', { roles: { 'default': [ 'admin' ] } });
return fn.call({ userId: user._id }, ...args);
};
});
};
But it only works when calling the methods directly.
I'm not sure how I can test my client code with correct security validations. How can I test my client code with authenticated users?
Part I: Making the function an exported function
You just need to add the exported method also to meteor methods.
imports/api/foo.js
export const foo = function(param1, param2){
if (!Roles.userIsInRole(this.userId, [ ...roles ], group)) {
throw new Meteor.Error('restricted', 'Access denied');
}
//....and other code
};
This method can then be imported in your server script:
imports/startup/methods.js
import {foo} from '../api/foo.js'
Meteor.methods({
'foo' : foo
});
So it is available to be called via Mateor.call('foo'...). Note that the callback has not to be defined in foo's function header, since it is wrapped automatically by meteor.
imports/api/foo.tests.js
import {foo} from './foo.js'
if (Meteor.isServer) {
// ... your test setup
const result = foo(...) // call foo directly in your test.
}
This is only on the server, now here is the thing for testing on the client: you will not come around calling it via Meteor.call and test the callback result. So on your client you still would test like:
imports/api/foo.tests.js
if (Meteor.isClient) {
// ... your test setup
Meteor.call('foo', ..., function(err, res) {
// assert no err and res...
});
}
Additional info:
I would advice you to use mdg:validated-method which allows the same functionality above PLUS gives you more sophisticated control over method execution, document schema validation and flexibility. It is also documented well enough to allow you to implement your above described requirement.
See: https://github.com/meteor/validated-method
Part II: Running you integration test with user auth
You have two options here to test your user authentication. They have both advantages and disadvantages and there debates about what is the better approach. No mater which one of both you will test, you need to write a server method, that adds an existing user to given set of roles.
Approach 1 - Mocking Meteor.user() and Meter.userid()
This is basically described/discussed in the following resources:
A complete gist example
An example of using either mdg:validated-method or plain methods
Using sinon spy and below also an answer from myself by mocking it manually but this may not apply for your case because it is client-only. Using sinon requires the following package: https://github.com/practicalmeteor/meteor-sinon
Approach 2 - Copying the "real" application behavior
In this case you completely test without mocking anything. You create real users and use their data in other tests as well.
In any case you need a server method, that creates a new user by given name and roles. Note that it should only be in a file with .test.js as name. Otherwise it can be considered a risk for security.
/imports/api/accounts/accounts.tests.js
Meteor.methods({
createtestUser(name,password, roles, group);
const userId = Accounts.createUser({username:name, password:password});
Roles.addUserToRoles(userId, roles, group);
return userId;
});
Note: I often heard that this is bad testing, which I disagree. Especially integration testing should mime the real behavior as good as possible und should use less mocking/spying as unit tests do.