A testing framework for Project Zomboid mods. Write specs in Lua with familiar describe/it/assert syntax, run them inside the actual game.
- BDD-style syntax -
describe,it,contextblocks - Rich assertions - type checks, comparisons, pattern matching
- In-game execution - tests run in the actual PZ environment
- Auto-discovery - finds
*_spec.luafiles automatically - Log capture - shows relevant game logs on test failure
- CI-friendly - exit codes and structured output
- Ruby 2.7+
- Project Zomboid
- ZombieBuddy mod (provides the Lua API)
-
Clone or copy ZBSpec to your mods folder:
mods/ZBSpec/ -
Add ZBSpec to your mod's dependencies (or enable both mods)
Create spec/zbspec.yml in your mod directory:
startup_timeout: 120
spec_glob: spec/**/*_spec.lua
mods:
- YourModNameCreate spec/example_spec.lua:
describe("my feature", function()
it("works correctly", function()
assert.is_equal(4, 2 + 2)
end)
it("handles nil", function()
assert.is_nil(nil)
assert.is_not_nil("hello")
end)
end)
return ZBSpec.run()# From your mod directory
zbspec
# Or specify files
zbspec spec/my_spec.lua
# With verbose output (shows health checks)
zbspec -v-- Optional: require your mod's files
require "MyMod_Data"
describe("ModuleName", function()
describe("nested context", function()
it("does something", function()
-- assertions here
end)
end)
end)
-- IMPORTANT: Always end with this
return ZBSpec.run()assert.is_equal(expected, actual)assert.is_true(value)
assert.is_true(value, "custom message")
assert.is_false(value)assert.is_nil(value)
assert.is_not_nil(value)assert.is_table(value)
assert.is_number(value)
assert.is_string(value)
assert.is_function(value)
assert.is_boolean(value)assert.greater_than(threshold, actual) -- actual > threshold
assert.less_than(threshold, actual) -- actual < thresholdassert.matches("^Base%.", itemType) -- Lua pattern
assert.contains("needle", "haystackneedle") -- substring
assert.contains("value", {"value", "other"}) -- table contains
assert.has_key("key", {key = "value"}) -- table has keyassert.throws(function()
error("boom")
end)
assert.throws(function()
error("specific error")
end, "specific") -- error must contain this stringrequire "MyMod"
local player = getPlayer()
if not player then
return "getPlayer() returned nil - player not loaded"
end
describe("inventory", function()
it("can create items", function()
local item = instanceItem("Base.Axe")
assert.is_not_nil(item)
end)
it("tracks player data", function()
player:getModData().testValue = 42
assert.is_equal(42, player:getModData().testValue)
end)
end)
return ZBSpec.run()ZBSpec supports running the same spec files in singleplayer, multiplayer client, and server contexts. Use context-specific describe blocks to write tests that work everywhere.
Mark tests as pending (not implemented yet):
pending("future feature", function()
-- This won't run, just recorded as pending
end)Full zbspec.yml options:
# Timeout for game startup (seconds)
startup_timeout: 120
# Auto-shutdown game after specs
auto_shutdown: false
# Path to PZ (for auto-launch on macOS)
game_path: /Applications/Project Zomboid.app
# Root dir for versioned game configs (default: ~/projects/zomboid/versions)
game_versions_root: "~/projects/zomboid/versions"
# Version names (subdirs under game_versions_root); first is default
game_versions:
- 41
- 42.12
- 42.13
- unstable
# Enable debug mode
debug: true
# Spec file pattern
spec_glob: spec/**/*_spec.lua
# Mods to load (ZombieBuddy and ZBSpec added automatically)
mods:
- YourModName
# Multiplayer settings (for --mp and --client modes)
server_name: ZBSpecServer
server_ip: 127.0.0.1
server_port: 16261
username: ZBSpecPlayer
password: ""Organize specs by where they should run:
spec/
├── zbspec.yml # Config file
├── data_spec.lua # Root specs (shared, run everywhere)
├── client/
│ └── ui_spec.lua # Client-only specs (also run in SP)
├── server/
│ └── commands_spec.lua # Server-only specs
└── shared/
└── sync_spec.lua # Shared specs (run on both)
spec/client/- Client-only specs (also run in singleplayer)spec/server/- Server-only specs (dedicated server)spec/shared/- Shared specs (run on both client and server)spec/*_spec.lua- Root specs (treated as shared)
Usage: zbspec [options] [spec_files...]
-c, --config PATH Path to config file (default: spec/zbspec.yml)
-m, --mod-dir PATH Path to mod directory
-v, --verbose Show verbose output (including health checks)
--sp Force singleplayer mode
--server Run specs on dedicated server only
--client Run specs on MP client only (server must be running)
--mp Run specs on both server and client
-h, --help Show help
--version Show version
By default, ZBSpec auto-detects the run mode based on spec folders:
- If
spec/server/contains specs → runs in MP mode - If only
spec/client/or root specs → runs in SP mode
zbspec # Auto-detect mode from spec folders
zbspec --sp # Force singleplayer
zbspec --mp # Force multiplayer (server + client)
zbspec --server # Server specs only
zbspec --client # Client specs only (server must be running)🚀 PZ Spec Harness Starting
==================================================
✓ API ready
🧪 Running Spec Suite
==================================================
Lua Specs:
✓ spec/data_spec.lua
✓ spec/specimen_spec.lua
✗ spec/broken_spec.lua
Error: Spec returned: "MyFeature does something: expected 5, got 3"
Log during test:
LOG: General > MyMod loaded
ERROR: General > Something went wrong
==================================================
Passed: 2
Failed: 1
- Early returns for missing state - Check
getPlayer()and return error string if nil - Clean up test data - Reset
modDataafter tests that modify it - Use descriptive names - Test names appear in failure output
- One assertion per concept - Makes failures easier to diagnose
- ZScienceSkill - A full mod with ZBSpec tests (
spec/directory)
- ZombieBuddy - Java modding framework for PZ (provides the Lua API that ZBSpec uses)
MIT