1 // Copyright (c) 2017-2018 Matthew Brennan Jones <matthew.brennan.jones@gmail.com>
2 // Boost Software License - Version 1.0
3 // Behavior Driven Development for the D programming language
4 // https://github.com/workhorsy/BDD
5 
6 /++
7 Behavior Driven Development for the D programming language
8 
9 Home page:
10 $(LINK https://github.com/workhorsy/BDD)
11 
12 Version: 1.3.0
13 
14 License:
15 Boost Software License - Version 1.0
16 
17 Examples:
18 ----
19 import BDD;
20 
21 int add(int a, int b) {
22 	return a + b;
23 }
24 
25 unittest {
26 	describe("math#add",
27 		it("Should add positive numbers", delegate() {
28 			add(5, 7).shouldEqual(12);
29 		}),
30 		it("Should add negative numbers", delegate() {
31 			add(5, -7).shouldEqual(-2);
32 		})
33 	);
34 }
35 
36 // Prints the results of the tests
37 int main() {
38 	return BDD.printResults();
39 }
40 ----
41 +/
42 
43 
44 module BDD;
45 
46 
47 /++
48 Used to assert that one value is equal to another value.
49 
50 Params:
51  a = The value to test.
52  b = The value it should be equal to.
53  file = The file name that the assert failed in. Should be left as default.
54  line = The file line that the assert failed in. Should be left as default.
55 
56 Throws:
57  If values are not equal, will throw an AssertError with expected and actual
58  values.
59 
60 Examples:
61 ----
62 // Will throw an exception like "AssertError@example.d(6): <3> expected to equal <5>."
63 int z = 3;
64 z.shouldEqual(5);
65 ----
66 +/
67 void shouldEqual(T, U)(T a, U b, string message=null, string file=__FILE__, size_t line=__LINE__) {
68 	import std..string : format;
69 	import std.array : replace;
70 	import core.exception : AssertError;
71 
72 	if (a != b) {
73 		if (! message) {
74 			message = "<%s> expected to equal <%s>.".format(a, b);
75 		}
76 		message = message.replace("\r", "\\r").replace("\n", "\\n");
77 		throw new AssertError(message, file, line);
78 	}
79 }
80 
81 unittest {
82 	describe("BDD#shouldEqual",
83 		it("Should succeed when equal", delegate() {
84 			"abc".shouldEqual("abc");
85 		}),
86 		it("Should fail when NOT equal", delegate() {
87 			shouldThrow(delegate() {
88 				"abc".shouldEqual("xyz");
89 			}, "<abc> expected to equal <xyz>.");
90 		})
91 	);
92 }
93 
94 /++
95 Used to assert that one value is NOT equal to another value.
96 
97 Params:
98  a = The value to test.
99  b = The value it should NOT be equal to.
100  file = The file name that the assert failed in. Should be left as default.
101  line = The file line that the assert failed in. Should be left as default.
102 
103 Throws:
104  If values are NOT equal, will throw an AssertError with unexpected and
105  actual values.
106 
107 Examples:
108 ----
109 // Will throw an exception like "AssertError@example.d(6): <3> expected to NOT equal <3>."
110 int z = 3;
111 z.shouldNotEqual(3);
112 ----
113 +/
114 void shouldNotEqual(T, U)(T a, U b, string message=null, string file=__FILE__, size_t line=__LINE__) {
115 	import std..string : format;
116 	import std.array : replace;
117 	import core.exception : AssertError;
118 
119 	if (a == b) {
120 		if (! message) {
121 			message = "<%s> expected to NOT equal <%s>.".format(a, b);
122 		}
123 		message = message.replace("\r", "\\r").replace("\n", "\\n");
124 		throw new AssertError(message, file, line);
125 	}
126 }
127 
128 unittest {
129 	describe("BDD#shouldNotEqual",
130 		it("Should succeed when NOT equal", delegate() {
131 			"abc".shouldNotEqual("xyz");
132 		}),
133 		it("Should fail when equal", delegate() {
134 			shouldThrow(delegate() {
135 				"abc".shouldNotEqual("abc");
136 			}, "<abc> expected to NOT equal <abc>.");
137 		})
138 	);
139 }
140 
141 /++
142 Used to assert that one value is equal to null.
143 
144 Params:
145  a = The value that should equal null.
146  file = The file name that the assert failed in. Should be left as default.
147  line = The file line that the assert failed in. Should be left as default.
148 
149 Throws:
150  If value is NOT null, will throw an AssertError.
151 
152 Examples:
153 ----
154 // Will throw an exception like "AssertError@example.d(6): expected to be <null>."
155 string z = "blah";
156 z.shouldBeNull();
157 ----
158 +/
159 void shouldBeNull(T)(T a, string message=null, string file=__FILE__, size_t line=__LINE__) {
160 	import std..string : format;
161 	import std.array : replace;
162 	import core.exception : AssertError;
163 
164 	if (a !is null) {
165 		if (! message) {
166 			message = "expected to be <null>.";
167 		}
168 		message = message.replace("\r", "\\r").replace("\n", "\\n");
169 		throw new AssertError(message, file, line);
170 	}
171 }
172 
173 unittest {
174 	describe("BDD#shouldBeNull",
175 		it("Should succeed when it is null", delegate() {
176 			string value = null;
177 			value.shouldBeNull();
178 		}),
179 		it("Should fail when it is NOT null", delegate() {
180 			shouldThrow(delegate() {
181 				string value = "abc";
182 				value.shouldBeNull();
183 			}, "expected to be <null>.");
184 		})
185 	);
186 }
187 
188 /++
189 Used to assert that one value is NOT equal to null.
190 
191 Params:
192  a = The value that should NOT equal null.
193  file = The file name that the assert failed in. Should be left as default.
194  line = The file line that the assert failed in. Should be left as default.
195 
196 Throws:
197  If value is null, will throw an AssertError.
198 
199 Examples:
200 ----
201 // Will throw an exception like "AssertError@example.d(6): expected to NOT be <null>."
202 string z = null;
203 z.shouldNotBeNull();
204 ----
205 +/
206 void shouldNotBeNull(T)(T a, string message=null, string file=__FILE__, size_t line=__LINE__) {
207 	import core.exception : AssertError;
208 
209 	if (a is null) {
210 		if (! message) {
211 			message = "expected to NOT be <null>.";
212 		}
213 		throw new AssertError(message, file, line);
214 	}
215 }
216 
217 unittest {
218 	describe("BDD#shouldNotBeNull",
219 		it("Should succeed when it is NOT null", delegate() {
220 			"abc".shouldNotBeNull();
221 		}),
222 		it("Should fail when it is null", delegate() {
223 			shouldThrow(delegate() {
224 				string value = null;
225 				value.shouldNotBeNull();
226 			}, "expected to NOT be <null>.");
227 		})
228 	);
229 }
230 
231 /++
232 Used to assert that one value is in an array of specified values.
233 
234 Params:
235  value = The value to test.
236  valid_values = An array of valid values.
237  file = The file name that the assert failed in. Should be left as default.
238  line = The file line that the assert failed in. Should be left as default.
239 
240 Throws:
241  If the value is not in the array, will throw an AssertError with the value
242  and array values.
243 
244 Examples:
245 ----
246 // Will throw an exception like "AssertError@example.d(6): <Bobrick> is not in <[Tim, Al]>."
247 "Bobrick".shouldBeIn(["Tim", "Al"]);
248 ----
249 +/
250 void shouldBeIn(T, U)(T value, U[] valid_values, string file=__FILE__, size_t line=__LINE__) {
251 	import std..string : format;
252 	import std.array : replace, join;
253 	import core.exception : AssertError;
254 
255 	bool is_valid = false;
256 
257 	foreach (valid; valid_values) {
258 		if (value == valid) {
259 			is_valid = true;
260 		}
261 	}
262 
263 	if (! is_valid) {
264 		string message = "<%s> is not in <[%s]>.".format(value, valid_values.join(", "));
265 		message = message.replace("\r", "\\r").replace("\n", "\\n");
266 		throw new AssertError(message, file, line);
267 	}
268 }
269 
270 unittest {
271 	describe("BDD#shouldBeIn",
272 		it("Should succeed when the string is in the array", delegate() {
273 			"abc".shouldBeIn(["abc", "xyz"]);
274 		}),
275 		it("Should fail when the string is NOT in the array", delegate() {
276 			shouldThrow(delegate() {
277 				"qed".shouldBeIn(["abc", "xyz"]);
278 			}, "<qed> is not in <[abc, xyz]>.");
279 		})
280 	);
281 }
282 
283 /++
284 Used to assert that one value is greater than another value.
285 
286 Params:
287  a = The value to test.
288  b = The value it should be greater than.
289  file = The file name that the assert failed in. Should be left as default.
290  line = The file line that the assert failed in. Should be left as default.
291 
292 Throws:
293  If the value is NOT greater, will throw an AssertError with expected and actual
294  values.
295 
296 Examples:
297 ----
298 // Will throw an exception like "AssertError@example.d(6): <5> expected to be greater than <10>."
299 5.shouldBeGreater(10);
300 ----
301 +/
302 void shouldBeGreater(T, U)(T a, U b, string message=null, string file=__FILE__, size_t line=__LINE__) {
303 	import std..string : format;
304 	import std.array : replace;
305 	import core.exception : AssertError;
306 
307 	if (a <= b) {
308 		if (! message) {
309 			message = "<%s> expected to be greater than <%s>.".format(a, b);
310 		}
311 		message = message.replace("\r", "\\r").replace("\n", "\\n");
312 		throw new AssertError(message, file, line);
313 	}
314 }
315 
316 unittest {
317 	describe("BDD#shouldBeGreater",
318 		it("Should succeed when one is greater", delegate() {
319 			10.shouldBeGreater(5);
320 		}),
321 		it("Should fail when one is NOT greater", delegate() {
322 			shouldThrow(delegate() {
323 				5.shouldBeGreater(10);
324 			}, "<5> expected to be greater than <10>.");
325 		})
326 	);
327 }
328 
329 /++
330 Used to assert that one value is less than another value.
331 
332 Params:
333  a = The value to test.
334  b = The value it should be less than.
335  file = The file name that the assert failed in. Should be left as default.
336  line = The file line that the assert failed in. Should be left as default.
337 
338 Throws:
339  If the value is NOT less, will throw an AssertError with expected and actual
340  values.
341 
342 Examples:
343 ----
344 // Will throw an exception like "AssertError@example.d(6): <10> expected to be less than <5>."
345 10.shouldBeLess(5);
346 ----
347 +/
348 void shouldBeLess(T, U)(T a, U b, string message=null, string file=__FILE__, size_t line=__LINE__) {
349 	import std..string : format;
350 	import std.array : replace;
351 	import core.exception : AssertError;
352 
353 	if (a >= b) {
354 		if (! message) {
355 			message = "<%s> expected to be less than <%s>.".format(a, b);
356 		}
357 		message = message.replace("\r", "\\r").replace("\n", "\\n");
358 		throw new AssertError(message, file, line);
359 	}
360 }
361 
362 unittest {
363 	describe("BDD#shouldBeLess",
364 		it("Should succeed when one is less", delegate() {
365 			5.shouldBeLess(10);
366 		}),
367 		it("Should fail when one is NOT less", delegate() {
368 			shouldThrow(delegate() {
369 				10.shouldBeLess(5);
370 			}, "<10> expected to be less than <5>.");
371 		})
372 	);
373 }
374 
375 /++
376 Used to assert that one value is greater or equal than another value.
377 
378 Params:
379  a = The value to test.
380  b = The value it should be greater or equal than.
381  file = The file name that the assert failed in. Should be left as default.
382  line = The file line that the assert failed in. Should be left as default.
383 
384 Throws:
385  If the value is NOT greater or equal, will throw an AssertError with expected and actual
386  values.
387 
388 Examples:
389 ----
390 // Will throw an exception like "AssertError@example.d(6): <5> expected to be greater or equal to <10>."
391 5.shouldBeGreaterOrEqual(10);
392 ----
393 +/
394 void shouldBeGreaterOrEqual(T, U)(T a, U b, string message=null, string file=__FILE__, size_t line=__LINE__) {
395 	import std..string : format;
396 	import std.array : replace;
397 	import core.exception : AssertError;
398 
399 	if (a < b) {
400 		if (! message) {
401 			message = "<%s> expected to be greater or equal to <%s>.".format(a, b);
402 		}
403 		message = message.replace("\r", "\\r").replace("\n", "\\n");
404 		throw new AssertError(message, file, line);
405 	}
406 }
407 
408 unittest {
409 	describe("BDD#shouldBeGreaterOrEqual",
410 		it("Should succeed when one is greater", delegate() {
411 			10.shouldBeGreaterOrEqual(5);
412 		}),
413 		it("Should succeed when one is equal", delegate() {
414 			10.shouldBeGreaterOrEqual(10);
415 		}),
416 		it("Should fail when one is less", delegate() {
417 			shouldThrow(delegate() {
418 				5.shouldBeGreaterOrEqual(10);
419 			}, "<5> expected to be greater or equal to <10>.");
420 		})
421 	);
422 }
423 
424 /++
425 Used to assert that one value is less or equal than another value.
426 
427 Params:
428  a = The value to test.
429  b = The value it should be less or equal than.
430  file = The file name that the assert failed in. Should be left as default.
431  line = The file line that the assert failed in. Should be left as default.
432 
433 Throws:
434  If the value is NOT less or equal, will throw an AssertError with expected and actual
435  values.
436 
437 Examples:
438 ----
439 // Will throw an exception like "AssertError@example.d(6): <10> expected to be less or equal to <5>."
440 10.shouldBeLessOrEqual(5);
441 ----
442 +/
443 void shouldBeLessOrEqual(T, U)(T a, U b, string message=null, string file=__FILE__, size_t line=__LINE__) {
444 	import std..string : format;
445 	import std.array : replace;
446 	import core.exception : AssertError;
447 
448 	if (a > b) {
449 		if (! message) {
450 			message = "<%s> expected to be less or equal to <%s>.".format(a, b);
451 		}
452 		message = message.replace("\r", "\\r").replace("\n", "\\n");
453 		throw new AssertError(message, file, line);
454 	}
455 }
456 
457 unittest {
458 	describe("BDD#shouldBeLessOrEqual",
459 		it("Should succeed when one is less", delegate() {
460 			5.shouldBeLessOrEqual(10);
461 		}),
462 		it("Should succeed when one is equal", delegate() {
463 			10.shouldBeLessOrEqual(10);
464 		}),
465 		it("Should fail when one is less", delegate() {
466 			shouldThrow(delegate() {
467 				10.shouldBeLessOrEqual(5);
468 			}, "<10> expected to be less or equal to <5>.");
469 		})
470 	);
471 }
472 
473 /++
474 Used for asserting that a delegate will throw an exception.
475 
476 Params:
477  cb = The delegate that is expected to throw the exception.
478  message = The message that is expected to be in the exception. Will not
479 be tested, if it is null.
480  file = The file name that the assert failed in. Should be left as default.
481  line = The file line that the assert failed in. Should be left as default.
482 
483 Throws:
484  If delegate does NOT throw, will throw an AssertError.
485 
486 Examples:
487 ----
488 // Makes sure it throws with the message "boom!"
489 shouldThrow(delegate() {
490 	throw new Exception("boom!");
491 }, "boom!");
492 
493 // Makes sure it throws, but does not check the message
494 shouldThrow(delegate() {
495 	throw new Exception("boom!");
496 });
497 
498 // Will throw an exception like "AssertError@test/example.d(7): Exception was not thrown. Expected: boom!"
499 shouldThrow(delegate() {
500 
501 }, "boom!");
502 
503 // Will throw an exception like "AssertError@test/example.d(7): Exception was not thrown. Expected one.
504 shouldThrow(delegate() {
505 
506 });
507 ----
508 +/
509 void shouldThrow(void delegate() cb, string message=null, string file=__FILE__, size_t line=__LINE__) {
510 	import core.exception : AssertError;
511 
512 	bool has_thrown = false;
513 	try {
514 		cb();
515 	} catch (Throwable ex) {
516 		has_thrown = true;
517 		if (message && message != ex.msg) {
518 			throw new AssertError("Exception was thrown. But expected: " ~ message, file, line);
519 		}
520 	}
521 
522 	if (! has_thrown) {
523 		if (message) {
524 			throw new AssertError("Exception was not thrown. Expected: " ~ message, file, line);
525 		} else {
526 			throw new AssertError("Exception was not thrown. Expected one.", file, line);
527 		}
528 	}
529 }
530 
531 unittest {
532 	describe("BDD#shouldThrow",
533 		it("Should succeed when an exception is thrown", delegate() {
534 			bool has_thrown = false;
535 
536 			shouldThrow(delegate() {
537 				has_thrown = true;
538 				throw new Exception("boom!");
539 			});
540 
541 			has_thrown.shouldEqual(true);
542 		}),
543 		it("Should fail when an exception is not thrown", delegate() {
544 			Throwable ex = null;
545 			try {
546 				shouldThrow(delegate() {
547 					// Does not throw
548 				});
549 			} catch (Throwable exception) {
550 				ex = exception;
551 			}
552 
553 			ex.shouldNotBeNull();
554 			ex.msg.shouldEqual("Exception was not thrown. Expected one.");
555 		})
556 	);
557 }
558 
559 void beforeIt(void delegate() cb) {
560 	_before_it = cb;
561 }
562 
563 void afterIt(void delegate() cb) {
564 	_after_it = cb;
565 }
566 
567 
568 /++
569 Prints the results of all the tests. Returns 0 if all tests pass, or 1 if any fail.
570 
571 Examples:
572 ----
573 	BDD.printResults();
574 ----
575 
576 Output:
577 ----
578 Unit Test Results:
579 4 total, 2 successful, 2 failed
580 math#add
581 - "5 + 7 = 12: <12> expected to equal <123>." broken_math.d(2)
582 math#subtract
583 - "5 - 7 = -2: <-2> expected to equal <456>." broken_math.d(6)
584 ----
585 +/
586 int printResults() {
587 	import std.stdio : stdout;
588 
589 	stdout.writeln("Unit Test Results:");
590 	stdout.writefln("%d total, %d successful, %d failed", _success_count + _fail_count, _success_count, _fail_count);
591 
592 	foreach (a, b; _fail_messages) {
593 		stdout.writefln("%s", a);
594 		foreach (c; b) {
595 			stdout.writefln("- %s", c);
596 		}
597 	}
598 
599 	return _fail_count > 0;
600 }
601 
602 /++
603 The message is usually the name of the thing being tested.
604 
605 Params:
606  describe_message = The thing that is being described.
607  pairs = All the 'it' delegate functions that will test the thing.
608 
609 Examples:
610 ----
611 	describe("example_library#thing_to_test",
612 		it("Should NOT fail", delegate() {
613 			// code here
614 		})
615 	);
616 ----
617 +/
618 void describe(TestPair...)(string describe_message, TestPair pairs) {
619 	foreach (pair; pairs) {
620 		try {
621 			if (_before_it) {
622 				_before_it();
623 			}
624 
625 			pair.func();
626 
627 			if (_after_it) {
628 				_after_it();
629 			}
630 
631 			addSuccess();
632 		} catch (Throwable ex) {
633 			addFail(describe_message, pair, ex);
634 		}
635 	}
636 }
637 
638 /++
639 The message should describe what the test should do.
640 
641 Params:
642  message = The message to print when the test fails.
643  func = The delegate to call when running the test.
644 
645 Examples:
646 ----
647 int a = 4;
648 describe("example_library#a",
649 	it("Should equal 4", delegate() {
650 		a.shouldEqual(4);
651 	}),
652 	it("Should Not equal 5", delegate() {
653 		a.shouldNotEqual(5);
654 	})
655 );
656 ----
657 +/
658 TestPair it(string message, void delegate() func) {
659 	TestPair retval;
660 	retval.it_message = message;
661 	retval.func = func;
662 
663 	return retval;
664 }
665 
666 private struct TestPair {
667 	string it_message;
668 	void delegate() func;
669 }
670 
671 private void addSuccess() {
672 	_success_count++;
673 }
674 
675 private void addFail(string describe_message, TestPair pair, Throwable err) {
676 	import std..string : format;
677 
678 	_fail_messages[describe_message] ~= `"%s: %s" %s(%s)`.format(pair.it_message, err.msg, err.file, err.line);
679 	_fail_count++;
680 }
681 
682 private string[][string] _fail_messages;
683 private ulong _fail_count;
684 private ulong _success_count;
685 private void delegate() _before_it;
686 private void delegate() _after_it;
687 
688 /*
689 	TODO:
690 	* Make indentation in docs use 4 space sized tabs
691 */