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
In this tutorial we're going to see in detail how to: 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.Read more...
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
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/"]
}
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"]
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();
}
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();
});
});
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"
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:
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:
Feel free to follow me to get notified when new articles are out ;)
Top comments (1)
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.