DEV Community

Cover image for Write Unit Test for your Typescript GitHub Action
Leonardo Montini
Leonardo Montini

Posted on

Write Unit Test for your Typescript GitHub Action

GitHub Actions are a powerful tool to automate your workflow. They can be used to run tests, deploy your code, publish a package, and much more.

The cool thing is, there's a GitHub Actions Marketplace where you can find a lot of actions created by... the community.

But what if you can't find the action you need? You can create your own and publish it there!

How to use this tutorial

Read more...

In this tutorial we're going to see in detail how to:

  • Create a GitHub Action in Typescript
  • Expand our Action to support custom inputs
  • Integrate with GitHub's API to add labels to Pull Requests
  • Unit testing our action
  • Debugging in Visual Studio Code
  • Publishing our action to the GitHub Marketplace
  • Using our action in another repository
  • Some final touches to make our project more robust

The articles will be split into separate bite-sized chapters as technically each one can be a little tutorial by itself.

If you're interested in the full text all at once, you can find it here: https://leonardomontini.dev/typescript-github-action/

One more great piece of advice is to create a new repository and follow along with the steps. This way you'll have a working action at the end of the post and you'll be able to play with it and experiment, rather than just reading a long tutorial and forgetting about 90% of it.

The full code of this tutorial is available on GitHub on this repo, so you can always refer to it if you get stuck.

The full tutorial (all chapters at once) is also available as a video, which you can find here:

Chapter 5: Testing the action

You wouldn't push code into production without testing it, right? So let's write some tests for our action.

Setup

We'll use Jest to write our tests. It works out of the box with Javascript but needs a little bit of configuration to work with TypeScript.



npm install -D jest ts-jest @types/jest


Enter fullscreen mode Exit fullscreen mode

We also need to create a new config file for jest, in the root of our project, called jest.config.json.



{
  "preset": "ts-jest",
  "testEnvironment": "node",
  "collectCoverage": true,
  "coverageReporters": ["lcov", "text-summary"],
  "collectCoverageFrom": ["src/**/*.ts"],
  "coveragePathIgnorePatterns": ["/node_modules/", "/__tests__/"],
  "testPathIgnorePatterns": ["/node_modules/"]
}


Enter fullscreen mode Exit fullscreen mode

We're telling jest to use the ts-jest preset, to run the tests in node, to collect coverage and to ignore some files.

Additional changes

Without any extra configuration, the build will also include test files, which we don't want. On tsconfig.json, we can add a new exclude property.



"exclude": ["node_modules", "**/*.test.ts"]


Enter fullscreen mode Exit fullscreen mode

And also, if we'd run tests now, everything would run twice.

Why?

Because at the end of our index.ts file, we're calling the run function. This is the entry point of our action, and we want to run it when the action is triggered. However, we don't want it to run by default when we import the file in our tests.

A possible solution is to wrap the call to run in an if statement that checks if a jest environment variable is set.



if (!process.env.JEST_WORKER_ID) {
  run();
}


Enter fullscreen mode Exit fullscreen mode

Writing the tests

We can now create a new file called index.test.ts in the src/__tests__ folder. This is where we'll write our tests.

I have to say I'm a bit lazy so I asked GitHub Copilot to write the tests for me (guess what, I also made a video on this topic). At first, they were not passing, but after a few tweaks, I got them to pass. Here's an extract, but you can find the full file in the repository.



import { run } from '../index';
import { getInput, setFailed } from '@actions/core';
import { context, getOctokit } from '@actions/github';

// Mock getInput and setFailed functions
jest.mock('@actions/core', () => ({
  getInput: jest.fn(),
  setFailed: jest.fn(),
}));

// Mock context and getOctokit functions
jest.mock('@actions/github', () => ({
  context: {
    payload: {
      pull_request: {
        number: 1,
      },
    },
    repo: {
      owner: 'owner',
      repo: 'repo',
    },
  },
  getOctokit: jest.fn(),
}));

describe('run', () => {
  beforeEach(() => {
    // Clear all mock function calls and reset mock implementation
    jest.clearAllMocks();
  });

  it('should add label to the pull request', async () => {
    // Mock the return values for getInput
    (getInput as jest.Mock).mockReturnValueOnce('gh-token-value');
    (getInput as jest.Mock).mockReturnValueOnce('label-value');
    (context as any).payload.pull_request = {
      number: 1,
    };

    // Mock the Octokit instance and the addLabels method
    const mockAddLabels = jest.fn();
    const mockOctokit = {
      rest: {
        issues: {
          addLabels: mockAddLabels,
        },
      },
    };
    (getOctokit as jest.Mock).mockReturnValueOnce(mockOctokit);

    // Run the function
    await run();

    // Assertions
    expect(getInput).toHaveBeenCalledWith('gh-token');
    expect(getInput).toHaveBeenCalledWith('label');
    expect(getOctokit).toHaveBeenCalledWith('gh-token-value');
    expect(mockAddLabels).toHaveBeenCalledWith({
      owner: 'owner',
      repo: 'repo',
      issue_number: 1,
      labels: ['label-value'],
    });
    expect(setFailed).not.toHaveBeenCalled();
  });
});


Enter fullscreen mode Exit fullscreen mode

Running the tests

Now it's finally time to get rid of that "test": "echo \"Error: no test specified\" && exit 1" script in package.json. Just replace it with:



"test": "jest"


Enter fullscreen mode Exit fullscreen mode

This will run jest with the configuration we just created. If you want to run the tests in watch mode, you can use jest --watch.

We can now run npm test to run the tests. You should see something like this:

Tests running

Our action works as intended and we have some tests to prove it!

Closing

And that was it for today! if you have any question or suggestion, feel free to add a comment :)

See you in the next chapter!


Thanks for reading this article, I hope you found it interesting!

I recently launched my Discord server to talk about Open Source and Web Development, feel free to join: https://discord.gg/bqwyEa6We6

Do you like my content? You might consider subscribing to my YouTube channel! It means a lot to me ❤️
You can find it here:
YouTube

Feel free to follow me to get notified when new articles are out ;)

Top comments (1)

Collapse
 
cardinalby profile image
Cardinal

Take a look at github-action-ts-run-api. It's similar to your approach, but allows you to test Docker actions as well and also write integration tests.