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 }