GithubHelp home page GithubHelp logo

Comments (15)

ryparker avatar ryparker commented on August 16, 2024 4

Never mind I found a solution:

import * as github from '@actions/github'
import { GithubApi } from '../github';

// Shallow clone original @actions/github context
let originalContext = { ...github.context }

afterEach(() => {
  // Restore original @actions/github context
  Object.defineProperty(github, 'context', {
    value: originalContext,
  });
})


test('sets issueNumber, when initialized with an issue payload', () => {
  // Mock the @actions/github context.
  Object.defineProperty(github, 'context', {
    value: {
      payload: {
        issue: {
          number: 12345,
        },
      },
    },
  });
  const githubApi = new GithubApi('GITHUB_TOKEN');

  expect(githubApi['issueNumber']).toStrictEqual(12345);
});

from toolkit.

damccorm avatar damccorm commented on August 16, 2024 2

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

from toolkit.

damccorm avatar damccorm commented on August 16, 2024 2

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

from toolkit.

ericsciple avatar ericsciple commented on August 16, 2024 2

Example here: https://github.com/actions/checkout/blob/master/__test__/input-helper.test.ts

Mocks registered in beforeAll and unregistered in afterAll

from toolkit.

JasonEtco avatar JasonEtco commented on August 16, 2024 1

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

from toolkit.

xt0rted avatar xt0rted commented on August 16, 2024 1

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

from toolkit.

damccorm avatar damccorm commented on August 16, 2024 1

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!

from toolkit.

xt0rted avatar xt0rted commented on August 16, 2024 1

I'll give this a try later, thanks for the clarification on the setup.

from toolkit.

xt0rted avatar xt0rted commented on August 16, 2024

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

constructor() {
this.payload = process.env.GITHUB_EVENT_PATH
? require(process.env.GITHUB_EVENT_PATH)
: {}
this.eventName = process.env.GITHUB_EVENT_NAME as string
this.sha = process.env.GITHUB_SHA as string
this.ref = process.env.GITHUB_REF as string
this.workflow = process.env.GITHUB_WORKFLOW as string
this.action = process.env.GITHUB_ACTION as string
this.actor = process.env.GITHUB_ACTOR as string
}

While these return data, though issue is missing number since payload is undefined

get issue(): {owner: string; repo: string; number: number} {
const payload = this.payload
return {
...this.repo,
number: (payload.issue || payload.pullRequest || payload).number
}
}
get repo(): {owner: string; repo: string} {
if (process.env.GITHUB_REPOSITORY) {
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
return {owner, repo}
}
if (this.payload.repository) {
return {
owner: this.payload.repository.owner.login,
repo: this.payload.repository.name
}
}

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.

from toolkit.

xt0rted avatar xt0rted commented on August 16, 2024

@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
  }
}

from toolkit.

xt0rted avatar xt0rted commented on August 16, 2024

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

from toolkit.

damccorm avatar damccorm commented on August 16, 2024

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.

from toolkit.

ezyang avatar ezyang commented on August 16, 2024

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.

from toolkit.

ryparker avatar ryparker commented on August 16, 2024

Example here: actions/checkout@master/test/input-helper.test.ts

Mocks registered in beforeAll and unregistered in afterAll

@ericsciple This is useful however it doesn't demonstrate how we can mock context values such as issue, pull_request, etc.

Any guidance on how we might implement that with jest?

from toolkit.

Levelleor avatar Levelleor commented on August 16, 2024

Adding to the above answer - you could even take it one step further and reference an object here to have more convenient control over data in your tests:

// first define your mockContext somewhere
let mockContext= {value: 42};

// replace the github's context
Object.defineProperty(github, 'context', {
 get: () => mockContext,
 set: (newValue) => mockContext = newValue
});

// then in your tests you can control the context however you like
mockContexts.repo = { owner: 'some-owner' }

The only issue with this - I wasn't able to get it working without actually requiring the module from the test function. It will still reference the original context after replacement if you don't require it after setting the mock. But it works with imports as those use smarter logic in this regard.

from toolkit.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.