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