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