1 /** 2 * Mixin template to enable mocking. 3 * 4 * Many methods implement compile-time parameters (file, line) that are set at the call site. 5 * It is preferred that these parameters are ignored when using these methods. 6 * 7 * License: 8 * MIT. See LICENSE for full details. 9 */ 10 module dunit.mockable; 11 12 /** 13 * Imports. 14 */ 15 public import dunit.reflection; 16 17 /** 18 * A template mixin used to inject code into a class to provide mockable behaviour. 19 * Code is nested within the host class to provide access to all host types. 20 * $(B Only injects code when using the -unittest compiler switch.) 21 * 22 * Caveats: 23 * Only module level types can be made mockable. 24 * 25 * Example: 26 * --- 27 * import dunit.mockable; 28 * 29 * class T 30 * { 31 * mixin Mockable!(T); 32 * } 33 * --- 34 */ 35 public mixin template Mockable(C) if (is(C == class) || is(C == interface)) 36 { 37 version(unittest): 38 39 /* 40 * Struct for holding method count information. 41 */ 42 private struct MethodCount 43 { 44 /** 45 * The expected minimum count of the method. 46 */ 47 public ulong minimum; 48 49 /** 50 * The expected maximum count of the method. 51 */ 52 public ulong maximum; 53 54 /** 55 * The actual count of the method. 56 */ 57 public ulong actual; 58 } 59 60 /* 61 * Struct for holding the location of the disableParentMethods method. 62 */ 63 private struct FileLocation 64 { 65 /** 66 * The name of the file. 67 */ 68 public string file; 69 70 /** 71 * The line number in the file. 72 */ 73 public size_t line; 74 } 75 76 /** 77 * Injected by the Mockable mixin template this method allows creation of mock object instances of the mockable class. 78 * 79 * Params: 80 * args = The constructor arguments of the host class. 81 * 82 * Example: 83 * --- 84 * import dunit.mockable; 85 * 86 * class T 87 * { 88 * mixin Mockable!(T); 89 * } 90 * 91 * unittest 92 * { 93 * import dunit.toolkit; 94 * 95 * auto mock = T.getMock(); 96 * 97 * assertTrue(cast(T)mock); // Mock extends T. 98 * } 99 * --- 100 * 101 * Templates: 102 * 103 * Templated classes are supported when creating mocks. 104 * Simply include the template parameters for the class when calling the 'getMock' method. 105 * --- 106 * auto mock = T!(int).getMock(); // Get a mock of T!(int). 107 * --- 108 */ 109 static public auto getMock(A...)(A args) 110 { 111 return new Mock!(C)(args); 112 } 113 114 /** 115 * Injected by the Mockable mixin template this class contains all mocking behaviour. 116 * 117 * This Mock class extends any class it's injected into and provides methods to interact with the mocked instance. 118 * An instance of this class can dynamically replace any of its methods at runtime using the 'mockMethod' method. 119 * All mocked methods can optionally have their call counts asserted to be within set limits. 120 */ 121 private static class Mock(C) : C if (is(C == class) || is(C == interface)) 122 { 123 import dunit.error; 124 import dunit.moduleunittester; 125 import std.range; 126 import std..string; 127 import std.traits; 128 129 /* 130 * The friendly class name. 131 */ 132 private enum string className = C.stringof; 133 134 /* 135 * Records the call limits and call counts of each method. 136 */ 137 private MethodCount[string] _methodCount; 138 139 /* 140 * Boolean representing whether to use the parent methods or not. 141 */ 142 private bool _useParentMethods = true; 143 144 /* 145 * A structure to hold file location for the disableParentMethods method. 146 */ 147 private FileLocation _disableMethodsLocation; 148 149 /** 150 * Inject the necessary class code. 151 */ 152 static if (is(C == class)) 153 { 154 mixin(DUnitConstructorIterator!(C, "Constructor!(T, func)")); 155 } 156 mixin(DUnitMethodIterator!(C, "MethodDelegateProperty!(func)")); 157 mixin(DUnitMethodIterator!(C, "Method!(is(T == class), func)")); 158 159 /* 160 * Get the storage classes of the passed delegate. 161 * 162 * Params: 163 * method = The delegate to inspect. 164 * 165 * Returns: 166 * An array containing the storage classes of all delegate parameters. 167 */ 168 private string[] getStorageClasses(T)(T method) 169 { 170 string[] storageClasses; 171 string code; 172 173 foreach (storageClass; ParameterStorageClassTuple!(method)) 174 { 175 code = ""; 176 177 static if (storageClass == ParameterStorageClass.scope_) 178 { 179 code ~= "scope "; 180 } 181 182 static if (storageClass == ParameterStorageClass.lazy_) 183 { 184 code ~= "lazy "; 185 } 186 187 static if (storageClass == ParameterStorageClass.out_) 188 { 189 code ~= "out "; 190 } 191 192 static if (storageClass == ParameterStorageClass.ref_) 193 { 194 code ~= "ref "; 195 } 196 197 storageClasses ~= code; 198 } 199 return storageClasses; 200 } 201 202 /* 203 * Get the types of the passed delegate. 204 * 205 * Params: 206 * method = The delegate to inspect. 207 * 208 * Returns: 209 * An array containing the types of all delegate parameters. 210 */ 211 private string[] getTypes(T)(T method) 212 { 213 string[] types; 214 foreach (type; ParameterTypeTuple!(method)) 215 { 216 types ~= type.stringof; 217 } 218 return types; 219 } 220 221 /* 222 * Generate a signature using the passed name and method. The signature is used to 223 * match up delegates to methods behind the scenes. 224 * 225 * Params: 226 * name = The name of the method to generate the signature for. 227 * method = A delegate to inspect, retreiving parameter details. 228 * 229 * Returns: 230 * A string containing a method signature. 231 */ 232 private string generateSignature(T)(string name, T method) 233 { 234 string[] storageClasses = this.getStorageClasses!(T)(method); 235 string[] types = this.getTypes!(T)(method); 236 string[] parameters; 237 238 foreach (storageClass, type; zip(storageClasses, types)) 239 { 240 parameters ~= format("%s%s", storageClass, type); 241 } 242 243 return format("%s:%s(%s)", ReturnType!(method).stringof, name, parameters.join(", ")); 244 } 245 246 /** 247 * Replace a method in the mock object by adding a mock method to be called in its place. 248 * $(B By default parent methods are called in lieu of any replacements unless parent methods are disabled.) 249 * 250 * Params: 251 * name = The name of the method to replace. (Only the name should be used, no parameters, etc.) 252 * delegate_ = The delegate to be used instead of calling the original method. 253 * The delegate must have the exact signature of the method being replaced. 254 * minimumCount = The minimum amount of times this method must be called when asserting calls. 255 * maximumCount = The maximum amount of times this method must be called when asserting calls. 256 * file = The file name where the error occurred. The value is added automatically at the call site. 257 * line = The line where the error occurred. The value is added automatically at the call site. 258 * 259 * Throws: 260 * DUnitAssertError if the passed delegate does not match the signature of any overload of the method named. 261 * 262 * Caveats: 263 * $(OL 264 * $(LI In the case of replacing overloaded methods the delegate signature defines which method to replace. Helpful errors will be raised on non matching signatures.) 265 * $(LI Templated methods are final by default and therefore cannot be mocked.) 266 * ) 267 * 268 * See_Also: 269 * $(LINK2 mockable.html#disableParentMethods, disableParentMethods(...)) 270 * 271 * Example: 272 * --- 273 * import dunit.mockable; 274 * 275 * class T 276 * { 277 * int getValue() 278 * { 279 * return 1; 280 * } 281 * 282 * mixin Mockable!(T); 283 * } 284 * 285 * unittest 286 * { 287 * import dunit.toolkit; 288 * 289 * auto mock = T.getMock(); 290 * 291 * // Replace the 'getValue' method. 292 * mock.mockMethod("getValue", delegate(){ 293 * return 2; 294 * }); 295 * 296 * mock.getValue().assertEqual(2); 297 * } 298 * --- 299 */ 300 public void mockMethod(T)(string name, T delegate_, ulong minimumCount = 0, ulong maximumCount = ulong.max, string file = __FILE__, size_t line = __LINE__) 301 { 302 string signature = this.generateSignature!(T)(name, delegate_); 303 304 this._methodCount[signature] = MethodCount(minimumCount, maximumCount); 305 306 switch(signature) 307 { 308 mixin(DUnitMethodIterator!(C, "MethodSignatureSwitch!(func)")); 309 default: 310 auto error = new DUnitAssertError("Delegate does not match method signature", file, line); 311 error.addInfo("Method name", format("%s.%s", this.className, name)); 312 error.addError("Delegate signature", signature); 313 throw error; 314 } 315 } 316 317 /** 318 * Disable parent methods being called if mock replacements are not implemented. 319 * If parent methods have been disabled a helpful assert error will be raised on any attempt to call methods that haven't been replaced. 320 * This is helpful if it's necessary to disable all behaviour of the mocked class. 321 * 322 * Has no effect on mock objects derived from interfaces, by default all mocked interface methods assert an error until replaced. 323 * 324 * Params: 325 * file = The file name where the error occurred. The value is added automatically at the call site. 326 * line = The line where the error occurred. The value is added automatically at the call site. 327 * 328 * See_Also: 329 * $(LINK2 mockable.html#mockMethod, mockMethod(...)) 330 * 331 * Example: 332 * --- 333 * import dunit.mockable; 334 * 335 * class T 336 * { 337 * mixin Mockable!(T); 338 * } 339 * 340 * unittest 341 * { 342 * auto mock = T.getMock(); 343 * mock.disableParentMethods(); 344 * 345 * // All mock object methods that are used in the test 346 * // must now be replaced to avoid an error being thrown. 347 * } 348 * --- 349 */ 350 public void disableParentMethods(string file = __FILE__, size_t line = __LINE__) 351 { 352 this._disableMethodsLocation.file = file; 353 this._disableMethodsLocation.line = line; 354 this._useParentMethods = false; 355 } 356 357 /** 358 * Assert all replaced methods are called the defined amount of times. 359 * 360 * Params: 361 * message = The error message to display. 362 * file = The file name where the error occurred. The value is added automatically at the call site. 363 * line = The line where the error occurred. The value is added automatically at the call site. 364 * 365 * Throws: 366 * DUnitAssertError if any method was called outside of preset boundries. 367 * 368 * Example: 369 * --- 370 * import dunit.mockable; 371 * 372 * class T 373 * { 374 * int getValue() 375 * { 376 * return 1; 377 * } 378 * 379 * mixin Mockable!(T); 380 * } 381 * 382 * unittest 383 * { 384 * import dunit.toolkit; 385 * 386 * auto mock = T.getMock(); 387 * 388 * // Replace method while defining a minimum call limit. 389 * mock.mockMethod("getValue", delegate(){ 390 * return 2; 391 * }, 1); 392 * 393 * // Increase the call count of 'getValue' by one. 394 * mock.getValue().assertEqual(2); 395 * 396 * // Assert methods calls are within defined limits. 397 * mock.assertMethodCalls(); 398 * } 399 * --- 400 */ 401 public void assertMethodCalls(string message = "Failed asserting call count", string file = __FILE__, size_t line = __LINE__) 402 { 403 foreach (signature, methodCount; this._methodCount) 404 { 405 if (methodCount.actual < methodCount.minimum) 406 { 407 auto error = new DUnitAssertError(message, file, line); 408 409 error.addInfo("Method", format("%s.%s", this.className, signature)); 410 error.addExpectation("Minimum allowed calls", methodCount.minimum); 411 error.addError("Actual calls", methodCount.actual); 412 413 throw error; 414 } 415 416 if (methodCount.actual > methodCount.maximum) 417 { 418 auto error = new DUnitAssertError(message, file, line); 419 420 error.addInfo("Method", format("%s.%s", this.className, signature)); 421 error.addExpectation("Maximum allowed calls", methodCount.maximum); 422 error.addError("Actual calls", methodCount.actual); 423 424 throw error; 425 } 426 } 427 } 428 } 429 } 430 431 unittest 432 { 433 import dunit.toolkit; 434 435 interface T 436 { 437 public int method1(int param) const; 438 public void method2(int param) const; 439 public int method3(int param) const pure @safe nothrow; 440 public void method4(int param) const pure @safe nothrow; 441 public int method5(int param) const pure @trusted nothrow; 442 public void method6(int param) const pure @trusted nothrow; 443 444 mixin Mockable!T; 445 } 446 447 auto mock = T.getMock(); 448 449 mock.mockMethod("method1", delegate(int param) { return 2*param; }); 450 mock.mockMethod("method2", delegate(int param) { }); 451 mock.mockMethod("method3", delegate(int param) { return 2*param; }); 452 mock.mockMethod("method4", delegate(int param) { }); 453 mock.mockMethod("method5", delegate(int param) { return 2*param; }); 454 mock.mockMethod("method6", delegate(int param) { }); 455 456 mock.method1(10).assertEqual(20); 457 mock.method2(10); 458 mock.method3(10).assertEqual(20); 459 mock.method4(10); 460 mock.method5(10).assertEqual(20); 461 mock.method6(10); 462 } 463 464 unittest 465 { 466 import dunit.toolkit; 467 468 static class T 469 { 470 public int method1(int param) const { return 0; }; 471 public void method2(int param) const {}; 472 public int method3(int param) const pure @safe nothrow { return 0; }; 473 public void method4(int param) const pure @safe nothrow {}; 474 public int method5(int param) const pure @trusted nothrow { return 0; }; 475 public void method6(int param) const pure @trusted nothrow {}; 476 477 mixin Mockable!T; 478 } 479 480 auto mock = T.getMock(); 481 482 mock.mockMethod("method1", delegate(int param) { return 2*param; }); 483 mock.mockMethod("method2", delegate(int param) { }); 484 mock.mockMethod("method3", delegate(int param) { return 2*param; }); 485 mock.mockMethod("method4", delegate(int param) { }); 486 mock.mockMethod("method5", delegate(int param) { return 2*param; }); 487 mock.mockMethod("method6", delegate(int param) { }); 488 489 mock.method1(10).assertEqual(20); 490 mock.method2(10); 491 mock.method3(10).assertEqual(20); 492 mock.method4(10); 493 mock.method5(10).assertEqual(20); 494 mock.method6(10); 495 } 496 497 unittest 498 { 499 import dunit.toolkit; 500 501 static class T 502 { 503 public int method1() nothrow 504 { 505 assert(false, "thrown from method1"); 506 } 507 508 public void method2() 509 { 510 throw new Exception("thrown from method2"); 511 } 512 513 mixin Mockable!(T); 514 } 515 516 auto mock = T.getMock(); 517 518 mock.method1().assertThrow!Throwable("thrown from method1"); 519 mock.method2().assertThrow!Exception("thrown from method2"); 520 }