# mlua-lspec
BDD test framework for Lua on [mlua](https://github.com/mlua-rs/mlua).
Embeds a forked copy of [lust](https://github.com/bjornbytes/lust) and provides Rust APIs for executing Lua tests with structured result collection. Includes Rust-backed spy/stub/mock test doubles and fixture management.
## Quick start
```rust
let summary = mlua_lspec::run_tests(r#"
local describe, it, expect = lust.describe, lust.it, lust.expect
describe('math', function()
it('adds numbers', function()
expect(1 + 1).to.equal(2)
end)
end)
"#, "@test.lua").unwrap();
assert_eq!(summary.passed, 1);
assert_eq!(summary.failed, 0);
```
## Assertions
```lua
-- Equality
expect(value).to.equal(expected)
expect(value).to.equal(expected, epsilon) -- float tolerance
expect(value).to_not.equal(other)
-- Identity
expect(value).to.be(same_ref)
-- Truthiness / existence
expect(value).to.be.truthy()
expect(value).to.exist()
expect(nil).to_not.exist()
-- Type checking
expect(value).to.be.a('string')
expect(value).to.be.an('number')
-- Comparison
expect(10).to.be.gt(5) -- greater than
expect(10).to.be.gte(10) -- greater than or equal
expect(3).to.be.lt(5) -- less than
expect(3).to.be.lte(3) -- less than or equal
-- Tables
expect(tbl).to.have(value) -- contains value
expect(tbl).to.have_key('name') -- has key
expect(tbl).to.have_length(3) -- #tbl == 3
-- Strings
expect(str).to.have_length(5) -- #str == 5
expect(str).to.match('pattern') -- Lua pattern match
-- Errors
expect(fn).to.fail()
expect(fn).to.fail.with('pattern')
-- Negation (all assertions support to_not)
expect(value).to_not.equal(other)
expect(3).to_not.be.gt(5)
expect(tbl).to_not.have_key('missing')
```
## Test doubles
### Spy
Records calls while delegating to the original function.
```lua
local s = test_doubles.spy(function(x) return x * 2 end)
s(5)
s:call_count() -- 1
s:call_args(1) -- {5}
s:was_called_with(5) -- true
s:reset() -- clear call history
```
### Stub
Returns fixed values without calling any original.
```lua
local st = test_doubles.stub()
st:returns(42)
st() -- 42
```
### Spy on table method
Replaces a table method with a spy that calls through. Supports `revert()`.
```lua
local spy = test_doubles.spy_on(obj, "method_name")
obj.method_name("arg")
spy:call_count() -- 1
spy:revert() -- restore original
```
### Mock
Extends spy with declarative expectations verified at test end.
```lua
local m = test_doubles.mock(function(x) return x * 2 end)
m:expect_call_count(2)
m:expect_called_with(5)
m(5)
m(10)
m:verify() -- passes: called 2 times, once with arg 5
```
Mock methods:
| `mock(fn?)` | Create mock (optional call-through function) |
| `expect_call_count(n)` | Expect exactly `n` calls |
| `expect_called_with(args...)` | Expect at least one call with these args |
| `expect_never_called()` | Expect zero calls |
| `verify()` | Check all expectations (errors if unmet) |
| `clear_expectations()` | Remove all pending expectations |
| `call_count()`, `call_args(n)`, `was_called_with(args...)` | Spy-compatible inspection |
| `returns(val...)`, `reset()` | Spy-compatible mutation |
### Fixture
Managed test data store with temporary directory support.
```lua
local fix = test_doubles.fixture()
-- Key/value data (any Lua type including tables)
fix:set("user", { name = "alice", age = 30 })
fix:get("user").name -- "alice"
fix:has("user") -- true
fix:remove("user") -- returns the value
fix:count() -- number of entries
fix:keys() -- list of keys
-- File loading
fix:load_file("config", "path/to/config.txt")
fix:get("config") -- file contents as string
-- Temporary directories (auto-cleaned on GC or cleanup())
local dir = fix:tmpdir("work")
-- dir = "/tmp/lspec-work-12345-0"
-- Explicit cleanup (also happens on GC)
fix:cleanup()
```
## Granular control
For pre-existing Lua VMs (e.g. testing Rust APIs exposed to Lua):
```rust
use mlua::prelude::*;
let lua = Lua::new();
// Register your application globals
lua.globals().set("my_api", /* ... */).unwrap();
// Register lspec
mlua_lspec::register(&lua).unwrap();
mlua_lspec::register_doubles(&lua).unwrap();
// Run tests
lua.load(r#"
local describe, it, expect = lust.describe, lust.it, lust.expect
describe('my_api', function()
it('works', function()
expect(my_api.ping()).to.equal("pong")
end)
end)
"#).exec().unwrap();
let summary = mlua_lspec::collect_results(&lua).unwrap();
assert_eq!(summary.failed, 0);
```
## License
MIT OR Apache-2.0