v1.4.9: Validation fixes, Profiler fix, Quest link duplicate fix, Ascension fixes

This commit is contained in:
Xurkon
2026-03-26 06:47:54 -05:00
parent 284f0c3794
commit 1fc9727fee
194 changed files with 21008 additions and 743 deletions
+397
View File
@@ -0,0 +1,397 @@
---
paths:
- "**/tests/**/*.lua"
- "**/*_spec.lua"
- "**/*_test.lua"
---
# Lua Testing
> This file extends [common/testing.md](../common/testing.md) with Lua specific content.
## Test Frameworks
| Framework | Best for | Install |
|-----------|---------|---------|
| **Busted** | BDD-style unit/integration testing (most popular) | `luarocks install busted` |
| **LuaUnit** | xUnit-style, zero dependencies, minimal footprint | `luarocks install luaunit` |
| **Telescope** | Flexible, extensible, custom reporters | `luarocks install telescope` |
| **WoW mocks** | Game addon testing with C_API stubs | Custom `tests/mocks/` |
**Default choice**: Use **Busted** unless the project has an existing convention.
## Test Organization
```text
project/
├── src/ # Production code
│ ├── Database/
│ │ └── QuestieDB.lua
│ └── Modules/
│ └── QuestieInit.lua
├── tests/
│ ├── mocks/ # Environment stubs
│ │ ├── wow_api.lua # CreateFrame, C_Timer, GetTime stubs
│ │ ├── questie_env.lua # QuestieLoader, Questie object stubs
│ │ └── saved_vars.lua # SavedVariables mock
│ ├── unit/ # Unit tests (one per module)
│ │ ├── QuestieDB_spec.lua
│ │ ├── ZoneDB_spec.lua
│ │ └── compiler_spec.lua
│ ├── integration/ # Multi-module interaction tests
│ │ └── init_flow_spec.lua
│ └── helpers/ # Shared test utilities
│ ├── assertions.lua # Custom assert functions
│ └── fixtures.lua # Reusable test data
├── .busted # Busted configuration
└── .luacov # Coverage configuration
```
- Name spec files with `_spec.lua` suffix (Busted convention) or `_test.lua`
- Mirror the source directory structure in `tests/unit/`
- One spec file per production module
## Busted Configuration (`.busted`)
```lua
return {
_all = {
coverage = false,
lpath = "src/?.lua;src/?/init.lua",
},
default = {
verbose = true,
output = "utfTerminal",
ROOT = { "tests/" },
},
ci = {
ROOT = { "tests/" },
output = "TAP",
coverage = true,
},
}
```
## Unit Test Patterns
### Basic describe / it / before / after
```lua
describe("QuestieDB", function()
local QuestieDB
before_each(function()
-- Fresh module instance per test (isolation)
package.loaded["Database.QuestieDB"] = nil
QuestieDB = require("Database.QuestieDB")
QuestieDB.npcData = {
[1] = { "Hogger", 11, 0 },
[26680] = { "Grizzly Hills NPC", 74, 1 },
}
end)
after_each(function()
package.loaded["Database.QuestieDB"] = nil
end)
describe(":GetNPC", function()
it("returns NPC data for a valid id", function()
local data = QuestieDB:GetNPC(1)
assert.is_not_nil(data)
assert.are.equal("Hogger", data.name)
end)
it("returns nil for an unknown id", function()
assert.is_nil(QuestieDB:GetNPC(99999))
end)
it("returns nil for nil input", function()
assert.is_nil(QuestieDB:GetNPC(nil))
end)
it("returns nil for non-numeric input", function()
assert.is_nil(QuestieDB:GetNPC("abc"))
end)
end)
end)
```
### Pending Tests (Work-in-Progress)
Mark incomplete tests with `pending` — they appear in reports but do not fail:
```lua
pending("respects NPC blacklist during query")
pending("handles WotLK-only NPCs when plugin is absent")
```
### Test Naming Conventions
Use descriptive names that explain the scenario and expected outcome:
```lua
-- GOOD: Explains what + when + expected outcome
it("returns nil when NPC id does not exist in the database", ...)
it("triggers recompile when WotLK plugin is newly installed", ...)
it("skips cleanup when database has not been compiled yet", ...)
-- BAD: Vague
it("works", ...)
it("test 1", ...)
```
## Assertions Reference (Busted)
| Assertion | Meaning |
|-----------|---------|
| `assert.are.equal(expected, actual)` | Strict equality (`==`) |
| `assert.are.same(t1, t2)` | Deep table value equality |
| `assert.is_nil(v)` | `v == nil` |
| `assert.is_not_nil(v)` | `v ~= nil` |
| `assert.is_true(v)` | `v == true` (strict, not truthy) |
| `assert.is_false(v)` | `v == false` (strict, not falsy) |
| `assert.truthy(v)` | `v` is truthy (not `nil`/`false`) |
| `assert.falsy(v)` | `v` is falsy (`nil` or `false`) |
| `assert.has_error(fn)` | `fn()` throws any error |
| `assert.has_error(fn, "msg")` | `fn()` throws with specific message |
| `assert.has_no_error(fn)` | `fn()` does not throw |
| `assert.are.near(expected, actual, tolerance)` | Float comparison within epsilon |
## Mocking and Spies
### spy.on — Observe without replacing
```lua
local s = spy.on(Questie, "Debug")
QuestieDB:GetNPC(0)
assert.spy(s).was.called()
assert.spy(s).was.called_with(
match._, -- self (Questie)
Questie.DEBUG_CRITICAL, -- severity
match._ -- message string
)
s:revert() -- Restore original (automatic in after_each)
```
### stub — Replace with controlled implementation
```lua
stub(QuestieDB, "QueryNPCSingle").returns(nil)
-- Verify it was called with specific args
assert.stub(QuestieDB.QueryNPCSingle).was.called_with(
match._, 26680, match._
)
QuestieDB.QueryNPCSingle:revert()
```
### mock — Full module replacement
```lua
local mockDB = mock({
GetNPC = function(_, id) return { name = "Mock NPC " .. id } end,
GetQuest = function() return nil end,
})
```
**Rule**: Always restore stubs and spies. Busted auto-reverts stubs created with `stub()` at the end of each `it()` block, but manually-created stubs need explicit `:revert()`.
## WoW API Mock Environment
Create a `tests/mocks/wow_api.lua` that defines the WoW protected API surface. Require it BEFORE any addon file:
```lua
-- tests/mocks/wow_api.lua
-- Frame system
_G.CreateFrame = function(frameType, name, parent, template)
local frame = {
_events = {},
_scripts = {},
RegisterEvent = function(self, event) self._events[event] = true end,
UnregisterEvent = function(self, event) self._events[event] = nil end,
SetScript = function(self, handler, fn) self._scripts[handler] = fn end,
GetScript = function(self, handler) return self._scripts[handler] end,
Show = function() end,
Hide = function() end,
IsShown = function() return true end,
}
return frame
end
-- Timer system
_G.C_Timer = {
After = function(delay, fn) fn() end, -- Execute immediately in tests
NewTicker = function(interval, fn, iterations)
for i = 1, (iterations or 1) do fn() end
return { Cancel = function() end }
end,
}
-- Time
_G.GetTime = function() return os.clock() end
_G.time = os.time
-- Unit info
_G.UnitGUID = function(unit) return "Player-1234-ABCDEF" end
_G.UnitName = function(unit) return "TestPlayer" end
_G.UnitLevel = function(unit) return 80 end
_G.UnitFactionGroup = function(unit) return "Alliance", "Alliance" end
-- Map API
_G.C_Map = {
GetMapInfo = function(mapID) return { mapID = mapID, name = "Test Zone" } end,
GetBestMapForUnit = function(unit) return 1 end,
}
-- Enum system
_G.Enum = {
UIMapType = { Cosmic = 0, World = 1, Continent = 2, Zone = 3, Dungeon = 4 },
}
-- Misc
_G.IsAddOnLoaded = function(name) return true end
_G.GetAddOnInfo = function(name) return name, "Test Addon", "", true, "INSECURE" end
_G.InCombatLockdown = function() return false end
_G.debugstack = function(level) return "mock stack trace" end
_G.geterrorhandler = function() return print end
_G.hooksecurefunc = function(table, name, fn) end
_G.print = print
_G.wipe = function(t) for k in pairs(t) do t[k] = nil end return t end
_G.select = select
_G.format = string.format
_G.strsplit = function(sep, str)
local parts = {}
local pattern = "([^" .. sep .. "]+)"
for match in str:gmatch(pattern) do
table.insert(parts, match)
end
return unpack(parts)
end
-- SavedVariables
_G.QuestieSV = {}
```
### Firing Events in Tests
```lua
-- Helper to fire a WoW event on a frame mock
local function fireEvent(frame, event, ...)
if frame._events[event] then
local onEvent = frame._scripts["OnEvent"]
if onEvent then
onEvent(frame, event, ...)
end
end
end
-- Usage in test
it("handles PLAYER_LOGIN event", function()
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:SetScript("OnEvent", myHandler)
fireEvent(frame, "PLAYER_LOGIN")
-- assert expected side effects
end)
```
## Error Path Testing
**Always test error paths, not just happy paths:**
```lua
describe("error handling", function()
it("handles nil rawdata gracefully", function()
QuestieDB.npcData = {}
local result = QuestieDB:GetNPC(99999)
assert.is_nil(result)
end)
it("logs critical error for nil rawdata", function()
local s = spy.on(Questie, "Debug")
QuestieDB.npcData = {}
QuestieDB:GetNPC(99999)
assert.spy(s).was.called_with(
match._, Questie.DEBUG_CRITICAL, match.is_string()
)
end)
it("survives pcall on corrupted data", function()
QuestieDB.npcData = { [1] = "not a table" }
assert.has_no_error(function()
QuestieDB:GetNPC(1)
end)
end)
end)
```
## Coverage
Use **Luacov** for line coverage. Target ≥ 80%.
### Configuration (`.luacov`)
```lua
return {
statsfile = "luacov.stats.out",
reportfile = "luacov.report.out",
exclude = {
"tests/", -- Exclude test files from coverage
"Database/Data/.*Data", -- Exclude large static data tables
"Localization/Translations/", -- Exclude translation strings
},
include = {
"Database/",
"Modules/",
},
}
```
### Commands
```bash
# Run tests with coverage
busted --coverage
# Generate report
luacov
cat luacov.report.out
# Fail CI if below threshold
awk '/^Total/ { if ($4+0 < 80) { print "Coverage below 80%: " $4 "%"; exit 1 } }' luacov.report.out
```
## Test-Driven Development Workflow (Lua)
1. **RED** — Write the test first. It must FAIL.
2. **GREEN** — Write the minimum production code to make it pass.
3. **REFACTOR** — Clean up while keeping tests green.
4. **COVERAGE** — Verify ≥ 80% with `busted --coverage && luacov`.
```bash
# TDD cycle
busted tests/unit/QuestieDB_spec.lua # RED: expect failures
# ... write implementation ...
busted tests/unit/QuestieDB_spec.lua # GREEN: expect passes
# ... refactor ...
busted --coverage && luacov # COVERAGE: verify 80%+
```
## Testing Commands
```bash
busted # Run all tests
busted --verbose # Verbose output
busted tests/unit/ # Run only unit tests
busted --filter="GetNPC" # Run tests matching pattern
busted --tags="wotlk" # Run tagged tests only
busted --coverage # With coverage collection
busted --output=TAP # TAP format for CI
busted --shuffle # Randomize test order (detect coupling)
```
## Agent Support
- **tdd-guide** — Use proactively for new features; enforces write-tests-first workflow
- **code-reviewer** — Review test quality after writing tests