Within Red Hat, we previously had been working on a complete refactor of a custom GitHub action called build-chain. During the course of this project, however, we realized that the only way to perform any kind of end-to-end testing for this custom action was by running it manually on GitHub. We would have had to make multiple test repositories and fake pull requests (PRs), and then manually verify the output upon running the action. Too much manual work!
We noticed that this issue wasn’t just limited to custom actions but also affected any kind of workflow. We ran into this annoying problem of having to test it by pushing it to GitHub, triggering the workflow, manually checking the output, making fixes and repeating this process until we got it right. At the end of this vicious cycle, we ended up with a polluted git history consisting of multiple useless commits and workflow runs.
Wouldn’t nektos/act cover this case?
The command line interface (CLI) tool nektos/act is used to run GitHub actions locally. So it might seem that this tool resolves our issues, but it lacks certain important features:
- There is no API for it (i.e. it is difficult to interact with this tool programmatically)
- There is no option to mock APIs during a workflow run
- There is no option to mock an entire step during a workflow run
- It runs the workflow using the actual repository, risking making actual changes to it (for example, running a workflow which pushes to a branch)
This led to the creation of mock-github and act-js—a one-stop solution for creating a local git environment to run our GitHub actions locally and programmatically.
Mock-github
Mock-github is a Node.JS library that allows you to configure and make completely local and functioning git repositories. In these repositories, you can add, commit and push files, create branches, merge branches, etc. Moreover, it provides an Octokit-like interface called Moctokit for mocking your GitHub API calls. For the sake of consistency, it also provides an interface called Mockapi to mock any APIs using the same interface that Moctokit uses (a use case for this will become more apparent later in this post).
Act-js
Act-js is a Node.JS wrapper for the nektos/act CLI tool, and provides a way to programmatically run your GitHub actions locally and verify their output. Additionally, you can mock any API call that’s made during the workflow run using Moctokit and Mockapi. As an alternative, you can also mock the entire step itself.
An example
Let’s see an example of how we can use these two libraries to test a workflow file programmatically.
Consider the following simple workflow file:
name: Test on: push jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: github api call run: | result=$(curl -s http://api.github.com/repos/owner/project) echo "$result" - name: custom api call run: | result=$(curl -s http://google.com) echo "$result"
It checks out the repository and makes two API calls—one to GitHub and one to Google.
Thanks to mock-github and Act-js, it’s now possible to test this workflow using Jest in a TypeScript environment. The first thing to do is create a local git repository in which we can run our GitHub action. Think of this as creating a clean standard environment to test your code in:
const github = new MockGithub({ repo: { foo: { files: [ { src: path.resolve(__dirname, "..", ".github"), dest: ".github", } ], }, }, }); await github.setup();
This creates a local repository called “foo”. Our workflow files are placed into the .github directory of the foo repository. In this case, only the workflow file is needed in our local repository, but you can copy over whichever file/directory you want to the local repository.
Now, we should also prepare to mock the API calls during our test workflow run. In this case, it might seem a bit silly to do so, but imagine a more complex workflow where it depends on the data these APIs sent back. If we do not mock these APIs, then the results of running the same test might differ from one run to another (for example, if the repository is deleted then the GitHub API will fail). Moreover, what if we have a strict API rate limit? We don’t want to waste them on testing. So, we will use Moctokit and Mockapi.
const moctokit = new Moctokit("http://api.github.com"); const mockapi = new Mockapi({ google: { baseUrl: "http://google.com", endpoints: { root: { get: { path: "/", method: "get", parameters: { query: [], path: [], body: [], }, }, }, }, }, });
Here we have initialized Moctokit to use “http://api.github.com” as our base URL, and we have initialized Mockapi with the schema of our custom API. We are now ready to execute our workflow locally using Act from the act-js library:
const act = new Act(github.repo.getPath("foo")); const result = await act.runEvent("push", { mockApi: [ moctokit.rest.repos .get({ owner: "owner", repo: "project" }) .setResponse({ status: 200, data: { full_name: "owner/project" } }), mockapi.mock.google.root .get() .setResponse({ status: 200, data: "mock response" }), ], });
First, we initialize Act to run only in the foo local repository. Next, we trigger the workflow using the push event and define two mocks for this workflow execution. The first one is used to mock the GitHub API. Whenever an API call is made to GitHub to get the details for the repository owner/project in the workflow, it will receive our mocked response back rather than actual data. The next mock is for the Google API, which also behaves the same way.
Finally, let's compare the result of the run with the expected output:
expect(result).toStrictEqual([ { name: "Main actions/checkout@v3", status: 0, output: "", }, { name: "Main github api call", status: 0, output: '{"full_name":"owner/project"}', }, { name: "Main custom api call", status: 0, output: "mock response", }, ]);
Our expected output consists of three steps (as seen in the workflow file). For the checkout step, we don’t expect any output. It should just successfully execute as indicated by status 0. The next two steps should print the data from our mocked response without failing. Notice that “Main” is added before each step. This indicates that these steps are not part of the pre or post section of the workflow.
Now, put all of this together in ci.test.ts file:
import { Act } from "@kie/act-js"; import { Mockapi, MockGithub, Moctokit } from "@kie/mock-github"; import path from "path"; let github: MockGithub; beforeEach(async () => { github = new MockGithub({ repo: { foo: { files: [ { src: path.resolve(__dirname, "..", ".github"), dest: ".github", }, ], }, }, }); await github.setup(); }); afterEach(async () => { await github.teardown(); }); test("api workflow", async () => { const moctokit = new Moctokit("http://api.github.com"); const mockapi = new Mockapi({ google: { baseUrl: "http://google.com", endpoints: { root: { get: { path: "/", method: "get", parameters: { query: [], path: [], body: [], }, }, }, }, }, }); const act = new Act(github.repo.getPath("foo")); const result = await act.runEvent("push", { mockApi: [ moctokit.rest.repos .get({ owner: "owner", repo: "project" }) .setResponse({ status: 200, data: { full_name: "owner/project" } }), mockapi.mock.google.root .get() .setResponse({ status: 200, data: "mock response" }), ], }); expect(result).toStrictEqual([ { name: "Main actions/checkout@v3", status: 0, output: "", }, { name: "Main github api call", status: 0, output: '{"full_name":"owner/project"}', }, { name: "Main custom api call", status: 0, output: "mock response", }, ]); });
Then run it:
$ npm test > non-custom-actions@1.0.0 test > jest PASS test/ci.test.ts ✓ api workflow (2365 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 3.24 s, estimated 5 s Ran all test suites.
Success! We can now more easily test any changes to our workflow file locally.
Conclusion
Mock-github and act-js enable us to take a test-driven approach to writing github actions while making sure that our git history remains clean.
Make sure to check out the repositories for these libraries along with some more complex examples:
저자 소개
Shubh Bapna is a Software Engineering Intern at Red Hat, where he is part of the Business Automation team.
유사한 검색 결과
채널별 검색
오토메이션
기술, 팀, 인프라를 위한 IT 자동화 최신 동향
인공지능
고객이 어디서나 AI 워크로드를 실행할 수 있도록 지원하는 플랫폼 업데이트
오픈 하이브리드 클라우드
하이브리드 클라우드로 더욱 유연한 미래를 구축하는 방법을 알아보세요
보안
환경과 기술 전반에 걸쳐 리스크를 감소하는 방법에 대한 최신 정보
엣지 컴퓨팅
엣지에서의 운영을 단순화하는 플랫폼 업데이트
인프라
세계적으로 인정받은 기업용 Linux 플랫폼에 대한 최신 정보
애플리케이션
복잡한 애플리케이션에 대한 솔루션 더 보기
오리지널 쇼
엔터프라이즈 기술 분야의 제작자와 리더가 전하는 흥미로운 스토리
제품
- Red Hat Enterprise Linux
- Red Hat OpenShift Enterprise
- Red Hat Ansible Automation Platform
- 클라우드 서비스
- 모든 제품 보기
툴
체험, 구매 & 영업
커뮤니케이션
Red Hat 소개
Red Hat은 Linux, 클라우드, 컨테이너, 쿠버네티스 등을 포함한 글로벌 엔터프라이즈 오픈소스 솔루션 공급업체입니다. Red Hat은 코어 데이터센터에서 네트워크 엣지에 이르기까지 다양한 플랫폼과 환경에서 기업의 업무 편의성을 높여 주는 강화된 기능의 솔루션을 제공합니다.