pub fn run_tests(code: &str, chunk_name: &str) -> Result<TestSummary, String>Expand description
Run Lua test code with the lust framework pre-loaded.
Creates a fresh Lua VM, registers lust and test doubles,
executes code, and returns the structured test summary.
Lua’s print is replaced with a no-op to prevent stdout
pollution. Callers who need console output should use
register on their own Lua instance where print
remains intact.
Examples found in repository?
examples/basic_bdd.rs (lines 7-50)
6fn main() {
7 let summary = mlua_lspec::run_tests(
8 r#"
9 local describe, it, expect = lust.describe, lust.it, lust.expect
10
11 describe('string utilities', function()
12 describe('upper', function()
13 it('converts lowercase to uppercase', function()
14 expect(string.upper("hello")).to.equal("HELLO")
15 end)
16
17 it('leaves uppercase unchanged', function()
18 expect(string.upper("HELLO")).to.equal("HELLO")
19 end)
20
21 it('handles empty string', function()
22 expect(string.upper("")).to.equal("")
23 end)
24 end)
25
26 describe('rep', function()
27 it('repeats string n times', function()
28 expect(string.rep("ab", 3)).to.equal("ababab")
29 end)
30
31 it('returns empty for zero repeats', function()
32 expect(string.rep("x", 0)).to.equal("")
33 end)
34 end)
35
36 describe('find', function()
37 it('returns start index', function()
38 local start = string.find("hello world", "world")
39 expect(start).to.equal(7)
40 end)
41
42 it('returns nil for no match', function()
43 local result = string.find("hello", "xyz")
44 expect(result).to_not.exist()
45 end)
46 end)
47 end)
48 "#,
49 "@basic_bdd.lua",
50 )
51 .expect("test execution failed");
52
53 println!(
54 "Results: {} passed, {} failed",
55 summary.passed, summary.failed
56 );
57 for test in &summary.tests {
58 let icon = if test.passed { "PASS" } else { "FAIL" };
59 println!(" [{icon}] {}: {}", test.suite, test.name);
60 if let Some(ref err) = test.error {
61 println!(" {err}");
62 }
63 }
64
65 assert_eq!(summary.failed, 0, "all tests should pass");
66}More examples
examples/test_doubles_usage.rs (lines 7-187)
6fn main() {
7 let summary = mlua_lspec::run_tests(
8 r#"
9 local describe, it, expect = lust.describe, lust.it, lust.expect
10
11 -- ================================================================
12 -- System under test: an event dispatcher
13 -- ================================================================
14 local function create_dispatcher()
15 local handlers = {}
16 return {
17 on = function(self, event, handler)
18 handlers[event] = handlers[event] or {}
19 table.insert(handlers[event], handler)
20 end,
21 emit = function(self, event, data)
22 if handlers[event] then
23 for _, h in ipairs(handlers[event]) do
24 h(data)
25 end
26 end
27 end,
28 handler_count = function(self, event)
29 return handlers[event] and #handlers[event] or 0
30 end,
31 }
32 end
33
34 -- ================================================================
35 -- Tests
36 -- ================================================================
37
38 describe('event dispatcher', function()
39
40 describe('spy: verifying handler calls', function()
41 it('calls registered handler on emit', function()
42 local d = create_dispatcher()
43 local handler = test_doubles.spy(function() end)
44
45 d:on("click", handler)
46 d:emit("click", { x = 10, y = 20 })
47
48 expect(handler:call_count()).to.equal(1)
49 end)
50
51 it('passes event data to handler', function()
52 local d = create_dispatcher()
53 local handler = test_doubles.spy(function() end)
54
55 d:on("click", handler)
56 d:emit("click", { x = 10, y = 20 })
57
58 -- was_called_with compares primitives; for tables, use call_args
59 local args = handler:call_args(1)
60 expect(args[1].x).to.equal(10)
61 expect(args[1].y).to.equal(20)
62 end)
63
64 it('calls multiple handlers in order', function()
65 local d = create_dispatcher()
66 local order = {}
67 local h1 = test_doubles.spy(function() table.insert(order, "first") end)
68 local h2 = test_doubles.spy(function() table.insert(order, "second") end)
69
70 d:on("click", h1)
71 d:on("click", h2)
72 d:emit("click", {})
73
74 expect(h1:call_count()).to.equal(1)
75 expect(h2:call_count()).to.equal(1)
76 expect(order).to.equal({"first", "second"})
77 end)
78
79 it('does not call handler for different event', function()
80 local d = create_dispatcher()
81 local handler = test_doubles.spy(function() end)
82
83 d:on("click", handler)
84 d:emit("hover", {})
85
86 expect(handler:call_count()).to.equal(0)
87 end)
88 end)
89
90 describe('stub: faking external services', function()
91 -- Simulate a module that calls an external API
92 local function fetch_user(api_client, user_id)
93 local response = api_client.get("/users/" .. user_id)
94 if response.status == 200 then
95 return response.body
96 end
97 return nil, "not found"
98 end
99
100 it('returns user data on success', function()
101 local api = { get = test_doubles.stub() }
102 api.get:returns({ status = 200, body = { name = "Alice", id = 1 } })
103
104 local user = fetch_user(api, 1)
105
106 expect(user).to.exist()
107 expect(user.name).to.equal("Alice")
108 expect(api.get:call_count()).to.equal(1)
109 end)
110
111 it('returns nil on not found', function()
112 local api = { get = test_doubles.stub() }
113 api.get:returns({ status = 404, body = nil })
114
115 local user, err = fetch_user(api, 999)
116
117 expect(user).to_not.exist()
118 expect(err).to.equal("not found")
119 end)
120 end)
121
122 describe('spy_on: intercepting existing methods', function()
123 it('records calls to existing method', function()
124 local logger = {
125 messages = {},
126 log = function(self, msg)
127 table.insert(self.messages, msg)
128 end,
129 }
130
131 local spy = test_doubles.spy_on(logger, "log")
132
133 -- Call through the original object
134 logger.log(logger, "hello")
135 logger.log(logger, "world")
136
137 -- Spy recorded the calls
138 expect(spy:call_count()).to.equal(2)
139
140 -- Original still worked (call-through)
141 expect(#logger.messages).to.equal(2)
142 end)
143
144 it('reverts to original after test', function()
145 local calculator = {
146 add = function(a, b) return a + b end,
147 }
148
149 local spy = test_doubles.spy_on(calculator, "add")
150 calculator.add(1, 2) -- recorded by spy
151 expect(spy:call_count()).to.equal(1)
152
153 spy:revert()
154
155 -- Now it's the original function again
156 local result = calculator.add(10, 20)
157 expect(result).to.equal(30)
158 end)
159 end)
160
161 describe('spy as stub: switching behavior', function()
162 it('spy can be converted to stub mid-test', function()
163 local counter = 0
164 local s = test_doubles.spy(function()
165 counter = counter + 1
166 return counter
167 end)
168
169 -- Initially calls through
170 expect(s()).to.equal(1)
171 expect(s()).to.equal(2)
172
173 -- Switch to stub behavior
174 s:returns(999)
175
176 -- Now returns fixed value
177 expect(s()).to.equal(999)
178 expect(s()).to.equal(999)
179
180 -- All 4 calls recorded
181 expect(s:call_count()).to.equal(4)
182 end)
183 end)
184 end)
185 "#,
186 "@test_doubles.lua",
187 )
188 .expect("test execution failed");
189
190 println!(
191 "{} passed, {} failed out of {} tests",
192 summary.passed, summary.failed, summary.total
193 );
194 for test in &summary.tests {
195 let icon = if test.passed { "PASS" } else { "FAIL" };
196 println!(" [{icon}] {}: {}", test.suite, test.name);
197 if let Some(ref err) = test.error {
198 println!(" {err}");
199 }
200 }
201
202 assert_eq!(summary.failed, 0, "all tests should pass");
203}