github.com/peterbe/gg2 is a command line app that compiles to a single binary called gg
. It's for working with git
branches (and pull requests) fast(er). The code itself is written in TypeScript and I've blogged about it here.
At the time of writing, the test suite is not complete. It only tests the following commands:
gg --version
gg --help
gg start
gg commit
These are easiest because they don't involve actually network talking to GitHub's API. Anyway, they are interactive. There are some pure TypeScript unit tests already but what I wanted was a decent end-to-end test suite that makes sure the compiled binary works. Somewhat confusingly, the test suite of choice is Bun's testing framework, using the $ Shell API. It's essentially bash
but controlled from a regular TypeScript function.
Tests are run with bun test
which runs all suites. You can do rapid development with bun test --watch
. The test depends on there existing a gg
executable just like it would be on your command line. In GitHub Actions, to make that available, it gets put into the $PATH
using:
bun run build:linux-x64
mv out/gg-linux-x64 /home/runner/.local/bin/gg
echo "/home/runner/.local/bin" >> $GITHUB_PATH
gg --version
Now, here's a snippet of the test suite:
test("version", async () => {
const output = await $`gg --version`.text()
expect(/\d+\.\d+\d+/.test(output)).toBe(true)
})
That particular test doesn't involve actually using git
in a real repo. Here's an (abbreviated) snippet of a test that involves actually using git
and gg
together:
test("create branch and commit", async () => {
await $`git init --initial-branch=main`.cwd(tempDir)
await $`git config --global init.defaultBranch main`.cwd(tempDir)
await $`git config --global user.email "you@example.com"`.cwd(tempDir)
await $`git config --global user.name "Your Name"`.cwd(tempDir)
await $`echo "# My Project" > README.md`.cwd(tempDir)
await $`git add README.md`.cwd(tempDir)
await $`echo "This is my new branch" | gg start`.cwd(tempDir)
const branchName = await $`git branch --show-current`.cwd(tempDir).text()
expect(branchName.trim()).toBe("this-is-my-new-branch")
})
That tempDir
comes from a beforeEach
callback:
tempDir = await mkdtemp(join(tmpdir(), "gg-test"))
It works. It's fast. It's easy to debug. If something goes wrong, I can go in to the afterEach
callback and comment out the deletion of the temporary directory. Then, I can manually go in to that directory to poke:
afterEach(async () => {
- await rm(tempDir, { recursive: true })
+ // await rm(tempDir, { recursive: true })
+ console.log("TEMP DIR WAS", tempDir)
})
Note that the gg start
command uses import { input } from "@inquirer/prompts"
to prompt the user for input. That can be satisfied using the pipe as seen in...
This is my new branch" | gg start
I haven't tested more complex inputs like the import { select } from "@inquirer/prompts"
yet.
Next challenge is going to be; how do you test commands line gg pr
which interacts with the GitHub API. Remains to be figured out!
Comments