1 module unit_threaded.mock; 2 3 import unit_threaded.should: fail; 4 import std.traits; 5 import std.typecons; 6 7 version(unittest) { 8 import unit_threaded.asserts; 9 import unit_threaded.should; 10 } 11 12 13 alias Identity(alias T) = T; 14 15 private string implMixinStr(T)() { 16 import std.array: join; 17 18 string[] lines; 19 20 foreach(m; __traits(allMembers, T)) { 21 22 alias member = Identity!(__traits(getMember, T, m)); 23 24 static if(__traits(isAbstractFunction, member)) { 25 26 enum parameters = Parameters!member.stringof; 27 enum returnType = ReturnType!member.stringof; 28 29 static if(is(ReturnType!member == void)) 30 enum returnDefault = ""; 31 else { 32 enum varName = m ~ `_returnValues`; 33 lines ~= returnType ~ `[] ` ~ varName ~ `;`; 34 lines ~= ""; 35 enum returnDefault = [` if(` ~ varName ~ `.length > 0) {`, 36 ` auto ret = ` ~ varName ~ `[0];`, 37 ` ` ~ varName ~ ` = ` ~ varName ~ `[1..$];`, 38 ` return ret;`, 39 ` } else`, 40 ` return ` ~ returnType ~ `.init;`]; 41 } 42 43 lines ~= `override ` ~ returnType ~ " " ~ m ~ typeAndArgsParens!(Parameters!member) ~ ` {`; 44 lines ~= ` calledFuncs ~= "` ~ m ~ `";`; 45 lines ~= ` calledValues ~= tuple` ~ argNamesParens(arity!member) ~ `.to!string;`; 46 lines ~= returnDefault; 47 lines ~= `}`; 48 lines ~= ""; 49 } 50 } 51 52 return lines.join("\n"); 53 } 54 55 private string argNamesParens(int N) @safe pure { 56 return "(" ~ argNames(N) ~ ")"; 57 } 58 59 private string argNames(int N) @safe pure { 60 import std.range; 61 import std.algorithm; 62 import std.conv; 63 return iota(N).map!(a => "arg" ~ a.to!string).join(", "); 64 } 65 66 private string typeAndArgsParens(T...)() { 67 import std.array; 68 import std.conv; 69 string[] parts; 70 foreach(i, t; T) 71 parts ~= T[i].stringof ~ " arg" ~ i.to!string; 72 return "(" ~ parts.join(", ") ~ ")"; 73 } 74 75 mixin template MockImplCommon() { 76 bool verified; 77 string[] expectedFuncs; 78 string[] calledFuncs; 79 string[] expectedValues; 80 string[] calledValues; 81 82 void expect(string funcName, V...)(V values) @safe pure { 83 import std.conv: to; 84 import std.typecons: tuple; 85 86 expectedFuncs ~= funcName; 87 static if(V.length > 0) 88 expectedValues ~= tuple(values).to!string; 89 else 90 expectedValues ~= ""; 91 } 92 93 void expectCalled(string func, string file = __FILE__, size_t line = __LINE__, V...)(V values) { 94 expect!func(values); 95 verify(file, line); 96 } 97 98 void verify(string file = __FILE__, size_t line = __LINE__) @safe pure { 99 import std.range; 100 import std.conv; 101 102 if(verified) 103 fail("Mock already verified", file, line); 104 105 verified = true; 106 107 for(int i = 0; i < expectedFuncs.length; ++i) { 108 109 if(i >= calledFuncs.length) 110 fail("Expected nth " ~ i.to!string ~ " call to " ~ expectedFuncs[i] ~ " did not happen", file, line); 111 112 if(expectedFuncs[i] != calledFuncs[i]) 113 fail("Expected nth " ~ i.to!string ~ " call to " ~ expectedFuncs[i] ~ " but got " ~ calledFuncs[i] ~ 114 " instead", 115 file, line); 116 117 if(expectedValues[i] != calledValues[i] && expectedValues[i] != "") 118 throw new UnitTestException([expectedFuncs[i] ~ " was called with unexpected " ~ calledValues[i], 119 " ".repeat.take(expectedFuncs[i].length + 4).join ~ 120 "instead of the expected " ~ expectedValues[i]] , 121 file, line); 122 } 123 } 124 } 125 126 struct Mock(T) { 127 128 MockAbstract _impl; 129 alias _impl this; 130 131 class MockAbstract: T { 132 import std.conv: to; 133 //pragma(msg, implMixinStr!T); 134 mixin(implMixinStr!T); 135 mixin MockImplCommon; 136 } 137 138 ~this() pure @safe { 139 if(!verified) verify; 140 } 141 142 void returnValue(string funcName, V...)(V values) { 143 enum varName = funcName ~ `_returnValues`; 144 foreach(v; values) 145 mixin(varName ~ ` ~= v;`); 146 } 147 } 148 149 auto mock(T)() { 150 auto m = Mock!T(); 151 m._impl = new Mock!T.MockAbstract; 152 return m; 153 } 154 155 156 @("mock interface positive test no params") 157 @safe pure unittest { 158 interface Foo { 159 int foo(int, string) @safe pure; 160 void bar() @safe pure; 161 } 162 163 int fun(Foo f) { 164 return 2 * f.foo(5, "foobar"); 165 } 166 167 auto m = mock!Foo; 168 m.expect!"foo"; 169 fun(m); 170 } 171 172 @("mock interface positive test with params") 173 @safe pure unittest { 174 import unit_threaded.asserts; 175 176 interface Foo { 177 int foo(int, string) @safe pure; 178 void bar() @safe pure; 179 } 180 181 int fun(Foo f) { 182 return 2 * f.foo(5, "foobar"); 183 } 184 185 { 186 auto m = mock!Foo; 187 m.expect!"foo"(5, "foobar"); 188 fun(m); 189 } 190 191 { 192 auto m = mock!Foo; 193 m.expect!"foo"(6, "foobar"); 194 fun(m); 195 assertExceptionMsg(m.verify, 196 " source/unit_threaded/mock.d:123 - foo was called with unexpected Tuple!(int, string)(5, \"foobar\")\n" 197 " source/unit_threaded/mock.d:123 - instead of the expected Tuple!(int, string)(6, \"foobar\")"); 198 } 199 200 { 201 auto m = mock!Foo; 202 m.expect!"foo"(5, "quux"); 203 fun(m); 204 assertExceptionMsg(m.verify, 205 " source/unit_threaded/mock.d:123 - foo was called with unexpected Tuple!(int, string)(5, \"foobar\")\n" 206 " source/unit_threaded/mock.d:123 - instead of the expected Tuple!(int, string)(5, \"quux\")"); 207 } 208 } 209 210 211 @("mock interface negative test") 212 @safe pure unittest { 213 interface Foo { 214 int foo(int, string) @safe pure; 215 } 216 217 auto m = mock!Foo; 218 m.expect!"foo"; 219 m.verify.shouldThrowWithMessage("Expected nth 0 call to foo did not happen"); 220 } 221 222 // can't be in the unit test itself 223 version(unittest) 224 private class Class { 225 abstract int foo(int, string) @safe pure; 226 final int timesTwo(int i) @safe pure nothrow const { return i * 2; } 227 int timesThree(int i) @safe pure nothrow const { return i * 3; } 228 } 229 230 @("mock class positive test") 231 @safe pure unittest { 232 233 int fun(Class f) { 234 return 2 * f.foo(5, "foobar"); 235 } 236 237 auto m = mock!Class; 238 m.expect!"foo"; 239 fun(m); 240 } 241 242 243 @("mock interface multiple calls") 244 @safe pure unittest { 245 interface Foo { 246 int foo(int, string) @safe pure; 247 int bar(int) @safe pure; 248 } 249 250 void fun(Foo f) { 251 f.foo(3, "foo"); 252 f.bar(5); 253 f.foo(4, "quux"); 254 } 255 256 auto m = mock!Foo; 257 m.expect!"foo"(3, "foo"); 258 m.expect!"bar"(5); 259 m.expect!"foo"(4, "quux"); 260 fun(m); 261 m.verify; 262 } 263 264 @("interface expectCalled") 265 @safe pure unittest { 266 interface Foo { 267 int foo(int, string) @safe pure; 268 void bar() @safe pure; 269 } 270 271 int fun(Foo f) { 272 return 2 * f.foo(5, "foobar"); 273 } 274 275 auto m = mock!Foo; 276 fun(m); 277 m.expectCalled!"foo"(5, "foobar"); 278 } 279 280 @("interface return value") 281 @safe pure unittest { 282 interface Foo { 283 int timesN(int i) @safe pure; 284 } 285 286 int fun(Foo f) { 287 return f.timesN(3) * 2; 288 } 289 290 auto m = mock!Foo; 291 m.returnValue!"timesN"(42); 292 immutable res = fun(m); 293 res.shouldEqual(84); 294 } 295 296 @("interface return values") 297 @safe pure unittest { 298 interface Foo { 299 int timesN(int i) @safe pure; 300 } 301 302 int fun(Foo f) { 303 return f.timesN(3) * 2; 304 } 305 306 auto m = mock!Foo; 307 m.returnValue!"timesN"(42, 12); 308 fun(m).shouldEqual(84); 309 fun(m).shouldEqual(24); 310 fun(m).shouldEqual(0); 311 } 312 313 314 auto mockStruct(T...)(T returns) { 315 316 struct Mock { 317 318 private MockImpl* _impl; 319 alias _impl this; 320 321 static struct MockImpl { 322 323 static if(T.length > 0) { 324 alias FirstType = typeof(returns[0]); 325 private FirstType[] _returnValues; 326 } 327 328 mixin MockImplCommon; 329 330 auto opDispatch(string funcName, V...)(V values) { 331 import std.conv: to; 332 import std.typecons: tuple; 333 calledFuncs ~= funcName; 334 calledValues ~= tuple(values).to!string; 335 static if(T.length > 0) { 336 if(_returnValues.length == 0) return typeof(_returnValues[0]).init; 337 auto ret = _returnValues[0]; 338 _returnValues = _returnValues[1..$]; 339 return ret; 340 } 341 } 342 } 343 } 344 345 Mock m; 346 m._impl = new Mock.MockImpl; 347 static if(T.length > 0) 348 foreach(r; returns) 349 m._impl._returnValues ~= r; 350 return m; 351 } 352 353 354 @("mock struct positive") 355 @safe pure unittest { 356 void fun(T)(T t) { 357 t.foobar; 358 } 359 auto m = mockStruct; 360 m.expect!"foobar"; 361 fun(m); 362 m.verify; 363 } 364 365 @("mock struct negative") 366 @safe pure unittest { 367 auto m = mockStruct; 368 m.expect!"foobar"; 369 assertExceptionMsg(m.verify, 370 " source/unit_threaded/mock.d:123 - Expected nth 0 call to foobar did not happen\n"); 371 372 } 373 374 375 @("mock struct values positive") 376 @safe pure unittest { 377 void fun(T)(T t) { 378 t.foobar(2, "quux"); 379 } 380 381 auto m = mockStruct; 382 m.expect!"foobar"(2, "quux"); 383 fun(m); 384 m.verify; 385 } 386 387 @("mock struct values negative") 388 @safe pure unittest { 389 void fun(T)(T t) { 390 t.foobar(2, "quux"); 391 } 392 393 auto m = mockStruct; 394 m.expect!"foobar"(3, "quux"); 395 fun(m); 396 assertExceptionMsg(m.verify, 397 " source/unit_threaded/mock.d:123 - foobar was called with unexpected Tuple!(int, string)(2, \"quux\")\n" 398 " source/unit_threaded/mock.d:123 - instead of the expected Tuple!(int, string)(3, \"quux\")"); 399 } 400 401 402 @("struct return value") 403 @safe pure unittest { 404 int fun(T)(T f) { 405 return f.timesN(3) * 2; 406 } 407 408 auto m = mockStruct(42, 12); 409 fun(m).shouldEqual(84); 410 fun(m).shouldEqual(24); 411 fun(m).shouldEqual(0); 412 m.expectCalled!"timesN"; 413 } 414 415 @("struct expectCalled") 416 @safe pure unittest { 417 void fun(T)(T t) { 418 t.foobar(2, "quux"); 419 } 420 421 auto m = mockStruct; 422 fun(m); 423 m.expectCalled!"foobar"(2, "quux"); 424 }