I haven't seen any tests or examples that show how you'd mock @actions/github using Jest. How would one go about doing this?
My action contains the following:
import { getInput } from "@actions/core";
import { context, GitHub } from "@actions/github";
const client = new GitHub(
getInput("repo-token", { required: true })
);
var actorAccess: Octokit.Response<CollaboratorPermissionLevel> = await this.client.repos.getCollaboratorPermissionLevel({
...context.repo,
username: context.actor
});
// ...
await this.client.reactions.createForIssueComment({
...context.repo,
comment_id: commentId,
content: this.reaction.type,
});
We don't provide a specific library for mocking, though I'm working on docs that should probably include that. There's kinda 2 sides to this.
The first is the context (e.g. context.repo, context.actor in this case). This one is easier to mock, you can just set the environment variables that it reads from which you can see here.
The second half of this is a little more challenging - mocking the client calls to the GitHub API. On that one, I'd probably defer to the Octokit rest folk (this library builds on top of their client). Their guidance - the best I've found is here. To summarize, it looks like they maybe want to build out a more complete testing framework, but right now they recommend using nock to mock the calls to the GitHub API
+1 for using nock wherever possible. I've written a ton of tests for code that uses the GitHub API and Octokit, and while mocking the Octokit SDK totally works, it means that tests become dependent on the SDK to have specific methods.
Once those methods change, its hard to update your tests and code at the same time to track those changes. With nock, you're writing tests that are more realistic in how your code interacts with the API.
@damccorm actions/javascript-template sets you up with Jest so that's what I was using.
I came across nock while looking at JasonEtco/actions-toolkit and Octokit since that's what my actions were originally using, just wasn't sure if that was the recommended way to test actions or if mocking the toolkit itself was a better route.
actions/javascript-template sets you up with Jest so that's what I was using.
Yep - to be clear, those aren't mutually exclusive - you can still use the jest framework and just nock specific http requests.
I'll try to add some docs that talk about this soon!
I started updating my tests to use the GITHUB_ environment variables and what I've found is any of them that are used in a constructor end up returning undefined but if they're used in a getter or function then they return a value.
These "properties" all return undefined in my tests https://github.com/actions/toolkit/blob/f66f5629b3f1303cf816b5e1823bae92ff02ece9/packages/github/src/context.ts#L22-L32
While these return data, though issue is missing number since payload is undefined https://github.com/actions/toolkit/blob/f66f5629b3f1303cf816b5e1823bae92ff02ece9/packages/github/src/context.ts#L34-L54
If I update this class to use getters instead of setting everything up in the constructor then things work as I'd expect and the custom payload gets loaded.
I'm assuming this is a result of how Jest is sandboxing the tests and/or the modules are being loaded, but it's definitely not the behavior I was expecting to see.
So I ran into this and I think I know why exactly this is happening, I should be clearer in my walkthrough docs though and will update accordingly.
Basically, the issue is that the GitHub class loads its properties (e.g. payload, eventName, sha, etc...) as soon as it is loaded. So the env needs to be set before we require the action that requires @actions/github - you can see I do that in my walkthrough - https://github.com/actions/toolkit/blob/getting-started-docs/docs/github-package.md#mocking-the-octokit-client
I'll give this a try later, thanks for the clarification on the setup.
@damccorm I just tested out requiring the context inside of my test and that doesn't seem to be working either. Here's the code I'm using right now:
import * as path from "path";
import { CommandHandler } from "../src/commandHandler";
import { Context } from "@actions/github/lib/context";
describe("commandHandler", () => {
beforeEach(() => {
process.env["GITHUB_EVENT_NAME"] = "issue_comment";
process.env["GITHUB_SHA"] = "cb2fd97b6eae9f2c7fee79d5a86eb9c3b4ac80d8";
process.env["GITHUB_REF"] = "refs/heads/master";
process.env["GITHUB_WORKFLOW"] = "Issue comments";
process.env["GITHUB_ACTION"] = "run1";
process.env["GITHUB_ACTOR"] = "test-user";
});
describe("process", () => {
it("doesn't work", () => {
process.env["GITHUB_EVENT_PATH"] = path.join(__dirname, "payloads", "created.json");
const context: Context = require("@actions/github").context;
console.info({ context });
});
});
});
console.log produces the following output:
{
context: Context {
payload: {},
eventName: undefined,
sha: undefined,
ref: undefined,
workflow: undefined,
action: undefined,
actor: undefined
}
}
@damccorm after playing around with this a bit more I found that if I get rid of the import { CommandHandler } from "../src/commandHandler"; line and then require that in each test after setting up the context, then the environment variables are loaded. This really over complicates the tests though, repeats a ton of code, and I lose type information for what I require(). At this point it seems easier to mock the class instead of trying to use the default behavior.
At this point it seems easier to mock the class instead of trying to use the default behavior.
Yeah, you're probably right. I'll look into updating the docs.
after playing around with this a bit more I found that if I get rid of the import { CommandHandler } from "../src/commandHandler"; line and then require that in each test after setting up the context, then the environment variables are loaded. This really over complicates the tests though, repeats a ton of code, and I lose type information for what I require(). At this point it seems easier to mock the class instead of trying to use the default behavior.
I think it's worse than that. Because node only ever loads a module once, once you load action/github for the first time, the particular environment variables you had used at that point of time are burned in. Which means if you try to write a second test with different environment variables, they will still be stuck with the first setting (unless jest is running your tests in multiple processes, which I suppose is what it does by default). The global singleton is really screwing us over; better to just pass the hydrated github object around.
Example here: https://github.com/actions/checkout/blob/master/__test__/input-helper.test.ts
Mocks registered in beforeAll and unregistered in afterAll
Most helpful comment
Yep - to be clear, those aren't mutually exclusive - you can still use the jest framework and just nock specific http requests.
I'll try to add some docs that talk about this soon!