A mock of the Hammerspoon hs global for
testing Spoons under busted.
It covers the APIs Spoons commonly share, with Hammerspoon's real event
constants and its own fnutils and inspect code. It is strict: reading an
hs.<name> it doesn't provide raises hs_mock: hs.<name> is not mocked, so a
test fails loudly on the first API it hasn't faked yet. Tests can add or replace
anything.
From luarocks.org, into a project-local tree so each repo pins its own version:
luarocks install --tree .rocks hs-mockOr pinned to a GitHub release, which attaches a rockspec pinned to its tag. With
mise, a deps task keeps it installed and skips once it is:
# mise.toml
[env]
HS_MOCK_VERSION = "1.0.0"
[tasks.deps]
run = "luarocks install --tree .rocks https://github.com/hugoh/hs-mock/releases/download/v$HS_MOCK_VERSION/hs-mock-$HS_MOCK_VERSION-1.rockspec"
sources = ["mise.toml"]
outputs = [".rocks/lib/luarocks/rocks-5.4/hs-mock/*/rock_manifest"]
[tasks.test]
depends = ["deps"]
run = "busted"Then point busted at the tree, and add .rocks/ to .gitignore:
-- .busted
return {
_all = {
lpath = ".rocks/share/lua/5.4/?.lua;.rocks/share/lua/5.4/?/init.lua",
},
}Build a fresh mock for each test:
local hs_mock = require("hs_mock")
local hs, ctl, Spoon
before_each(function()
hs, ctl = hs_mock.new()
_G.hs = hs
hs.midi = { devices = function() return {} end } -- add what the spoon also needs
Spoon = dofile("init.lua")
end)
it("polls every minute", function()
Spoon:start()
ctl.advance(60)
assert.same({ "polled" }, ctl.logged("info"))
end)The API reference lists what is mocked and
every ctl test control. It is generated from the
LuaLS annotations in lua/hs_mock.lua, so editors
running LuaLS also complete and type-check ctl once the install tree is on
the library path:
{ "workspace.library": [".rocks/share/lua/5.4"] }Generic fakes belong here: ones that record calls or hold simple state, with no opinion about a particular Spoon. A Spoon's tests keep their own fake, with a comment on why, when it is:
- a test assertion rather than a mock, such as checking a lookup is exact;
- deliberately unrealistic, such as a
json.decodethat passes tables through; - too deep to share honestly:
axuielementtrees,webviewwith its JavaScript bridge,canvasrendering.
mise run test # specs, plus the rock built and required from its own tree
mise run docs # build the API page into docs/ from the annotations
mise run vendor 1.1.1 # refresh vendored fnutils and inspectCommits follow Conventional Commits;
merging to main cuts a semver release, attaches the tag-pinned rockspec, and
uploads it to luarocks.org when the LUAROCKS_API_KEY repository secret is set
(the upload job skips with a notice otherwise).
MIT; see LICENSE. lua/hs_mock/fnutils.lua is Hammerspoon's
(MIT, © 2014-2025 its contributors) and lua/hs_mock/inspect.lua is
inspect.lua (MIT, © 2013 Enrique
García Cota), both vendored unchanged with their notices.