Skip to content

How to test custom JavaScript Github actions?

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?

Answer

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).