test_doubles_usage/
test_doubles_usage.rs1fn 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}