betterglobekey drives macOS through cgo — a CoreGraphics event tap, Text Input Sources, an AppKit HUD (how that compiles without Xcode is its own post) — and a third of its Go is tests. Those two facts don’t coexist through discipline; they coexist through design. A test cannot press the Globe key, cannot grant Accessibility, and cannot click the system dialog that asks for it. So the app is shaped around what a test can’t do: everything unautomatable is squeezed into a few hundred lines at the edge, and everything else doesn’t know macOS exists. Ports and adapters are usually sold as architecture taste. Here they’re the testing strategy — the only one available when your dependency is an operating system.
The domain shows the shape. The switching logic — what the Globe key actually does — is pure Go behind ports, and not just the obvious ones. Reading and selecting input sources is a port. So is the wall clock, because double-press detection is time math, and time math deserves a clock you control:
// InputSources is the port through which the switcher observes and controls
// the system's input sources.
type InputSources interface {
Current() string
All() []string
Select(id string)
}
// Clock is the port through which the switcher reads the current time.
type Clock interface {
Now() time.Time
}
The tests fake both: a fakeSources that records every Select call, and a fakeClock advanced by hand. A double press is two calls with one advance(250 * time.Millisecond) between them — not a time.Sleep and a prayer. One layer up, the daemon sees the keyboard itself as a port, and its fake is two channels: Listen closes one and blocks on the other, which is enough to prove the lifecycle starts and stops cleanly on a machine with no keyboard attached to the code at all.
The interesting decisions start where the fakes end — inside the cgo packages. You can’t unit-test a call into Carbon, but “untestable” is not one category. The suite splits the border into four tiers, each claiming no more than it can prove:
- Pure logic that happens to live near cgo is tested normally. The keyboard package maps modifier names to event-flag masks; the test exercises the lookup and the unknown-name fallback, and never installs the tap.
- Read-only calls get live smoke tests.
All,Current, andNamerun against the real Text Input Sources API on the CI runner. They’re deterministic for one reason: every macOS install has at least one input source.Selectis never called — the suite refuses to change the machine’s keyboard layout as a side effect of running. - Permission-gated calls get callability tests. Whether the process is trusted for Accessibility depends on the host’s TCC grant, so the test asserts nothing about the value:
// TestTrustedIsCallable exercises the Accessibility query. Its result depends
// on the host's grant (false in a plain test process, true under a granted
// daemon), so only its callability is asserted.
func TestTrustedIsCallable(_ *testing.T) {
_ = Trusted()
}
And Prompt is never exercised at all — it opens a system dialog, and CI would wait for a click that never comes.
- The rest is untested on purpose. Installing the tap, the Objective-C files, the HUD — a test cannot press fn. That code is held to one framework call per function, thin enough that a bug has nowhere to sit: it works on the first manual run or it fails loudly.
CI makes the border visible from the outside: every job that touches Go runs on macos-latest, and not only for the tests — cgo against CoreGraphics doesn’t compile anywhere else. The lint workflow carries the comment “macOS so golangci-lint can compile the macOS-only cgo.” When your linter needs a specific operating system, the border is real.
The usual question is “how do I mock cgo?” — and it’s the wrong question. You don’t mock the border. You move it: shrink the foreign side until there’s nothing left in it worth testing, then test everything that remains.