Write Unit Test for your Typescript GitHub Action
Tags: github, githubactions, testing, typescript
Published: 2023-08-17 15:11:00

**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

{% details 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](https://github.com/Balastrong/ga-label-issue), so you can always refer to it if you get stuck.

{% enddetails %}

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

{% youtube N26xgQ7kLKo %}

## 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](https://jestjs.io/) to write our tests. It works out of the box with Javascript but needs a little bit of configuration to work with TypeScript.

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

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

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

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.

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

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.

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

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

```ts
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();
  });
});
```

### 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:

```json
"test": "jest"
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e879fykfn8u5jlb2f2m4.png)

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](https://img.shields.io/badge/YouTube:%20Dev%20Leonardo-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UC-KqnO3ez7vF-kyIQ_22rdA?sub_confirmation=1)

Feel free to follow me to get notified when new articles are out ;)
{% embed https://dev.to/balastrong %}