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