├── .gitignore
├── LICENSE.md
├── README.md
├── package.json
└── source
└── specd
├── matchers.d
├── reporter.d
├── runner.d
├── specd.d
└── specification.d
/.gitignore:
--------------------------------------------------------------------------------
1 | .dub/
2 | *.o
3 | *.a
4 |
5 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # specd
2 |
3 | Write unit tests as specifications, not assertions.
4 |
5 | describe("a specifications DSL").as((it) {
6 | it.should("read as natural language", (so) {
7 | "DSL useage".must.contain("use");
8 |
9 | });
10 | it.should("not be clunky, therefore", "statements".must.not.equal("overly verbose"));
11 | });
12 |
13 | describe("specd")
14 | .should("be easy to use, yet expressive",
15 | "user".must.not.be.between("rock", "hard place"))
16 | ;
17 |
18 | ## Getting started
19 |
20 | Write specifications in a unittest block:
21 |
22 | unittest {
23 | describe("a string")
24 | .should("have a length property", "foo".length.must.equal(3))
25 | ;
26 | }
27 |
28 | To automatically run the tests with reporting, compile with version /specrunner/:
29 |
30 | dmd /source/ -unittest -version=specrunner
31 |
32 | or if you're using dub, create a configuration in package.json:
33 |
34 | "configurations": [
35 | {
36 | "name": "test",
37 | "versions": ["specrunner"],
38 | "targetType": "executable"
39 | }
40 | ]
41 |
42 | Compiling with /specrunner/ will add a simple main():
43 |
44 | version(specrunner) {
45 | int main() {
46 | return reportAllSpecs() ? 0 : 10;
47 | }
48 | }
49 |
50 | If you prefer to activate it yourself, just call reportAllSpecs(). It returns true if
51 | all tests succeded, false otherwise.
52 |
53 | ## Specifications
54 |
55 | A specification describes something, and so you begin by writing
56 |
57 | describe("Unit under test")
58 |
59 | Follow this with the actual specifications as chained method calls, like so:
60 |
61 | describe("Unit under test")
62 | .should("fulfill first requirement", (...test code...))
63 | .should("fulfill second requirement", (...test code...))
64 | ;
65 |
66 | ### Test as lazy expression
67 |
68 | This is the simplest test code, written as a lazy expression. It is suitable for tests
69 | where setup is a oneliner, typically functional code or value objects:
70 |
71 | describe("A vector")
72 | .should("implement vector addition", (vector(1,2,3) + vector(4,5,6)).must.equal(vector(5,7,9)) )
73 | ;
74 |
75 | ### Test as a delegate
76 |
77 | When you need to do more test setup, writing a test as a delegate allows that:
78 |
79 | describe("A loop")
80 | .should("run through the loop the specified times", (when) {
81 | int n = 0;
82 | foreach(i; 0..10) {
83 | n++;
84 | }
85 |
86 | n.must.equal(10);
87 |
88 | })
89 | ;
90 |
91 | ### Tests as an associative array of delegates
92 |
93 | Instead of chaining calls to should, you can use an associative array describing tests:
94 |
95 | describe("must matching").should([
96 | "work on string": {
97 | "foo".must.equal("foo");
98 | },
99 | "work on int": {
100 | 2.must.equal(2);
101 | },
102 | "work on double": {
103 | 1.3.must.equal(1.3);
104 | }
105 | ]);
106 |
107 | Note that these tests are executed in an unspecified order. If order is important, use should
108 | chaining or the ordered array described below.
109 |
110 | ### Tests as an ordered array of delegates
111 |
112 | When the execution order of tests is important, and you want to avoid should chaining, you can
113 | write the tests as an array of delegates. Each delegate takes an argument to the specification
114 | chain, allowing you to write them like:
115 |
116 | describe("A Specification with ordered parts").as(
117 | (it) { it.should("execute each part", (executionSequence++).must.equal(0)); },
118 | (it) { it.should("execute its parts in order", (executionSequence++).must.equal(1)); }
119 | );
120 |
121 |
122 | ## Matchers
123 |
124 | You can write tests using assert(), but that will terminate the test run on the first error. It is
125 | instead suggested to use the matchers provided by specd, written on the form
126 |
127 | a.must.equal(b);
128 |
129 | If the arguments do not match, a MatchException is thrown.
130 |
131 | ### Equal matches
132 |
133 | Any pair of types that can be compared for equality can be used in an equal match. For example, these
134 | matches pass.
135 |
136 | "foo".must.equal("foo");
137 | 1.must.equal(1.0);
138 | [1, 2, 3].must.equal([1f, 2f, 3f]);
139 |
140 | Another way of expressing this is:
141 |
142 | "foo".must == "foo";
143 | "bar".must.not == "foo";
144 |
145 | They are functionally equivalent.
146 |
147 | ### Comparison matches
148 |
149 | Any pair of types that can be compared against each other (>, >=, <, <=) can be used in a comparison match:
150 |
151 | 1.must.be.greater_than(0);
152 | "foo".must.not.be.less_than_or_equal_to("bar");
153 |
154 | If these are too wordy for your taste, you can use the alternative shorter form:
155 |
156 | 1.must.be_!"<" (2);
157 |
158 | Note the underscore, which is there to avoid a conflict with the no-args be(), and also to make the whole
159 | line a little more readable. The two forms are functionally equivalent, and are reported the same way,
160 | using words for the operator.
161 |
162 | ### Range matches
163 |
164 | Any types that can be compared for less than and greater than can be compared for a range:
165 |
166 | 12.must.be.between(1, 15);
167 | "c".must.be.between("a", "d");
168 | 2.must.between(2,3);
169 |
170 | The "be" is optional sugar to make the statement read better - it has no impact on functionality when
171 | used like that.
172 |
173 | ### Contain matches
174 |
175 | Any type where indexOf(A, B) is valid can be used to check for containment:
176 |
177 | [1,2,3].must.contain(2);
178 | "foo".must.contain("oo");
179 |
180 | ### Negating matches
181 |
182 | Any match can be negated using not:
183 |
184 | 1.must.not.equal(2);
185 | "foo".must.not.contain("bar");
186 |
187 | ### Approximate matches
188 |
189 | Floating point values can be compared with a variable precision, since absolute equality is often
190 | difficult to achieve for non-integer values:
191 |
192 | sqrt(2).must.approximate(1.4142, 0.0001);
193 |
194 | The second argument is the maximum variance. The above is exactly equivalent to writing:
195 |
196 | sqrt(2).must.be.between(1.4142-0.0001, 1.4142+0.0001);
197 |
198 | ### Matching boolean values
199 |
200 | Matching boolean values can be done with equals, but if you prefer, there is an alternative syntax:
201 |
202 | bool a = true;
203 | a.must.equal(true);
204 | a.must.be.True;
205 | a.must.not.be.False;
206 |
207 | ### Matching null values
208 |
209 | Any type that can be null can be matched for null:
210 |
211 | somePointer.must.not.be.Null;
212 | someObjectReference.must.be.Null;
213 |
214 | Note that you cannot use equals when testing for null, since testing for equality to null is not valid D.
215 |
216 | ### Matching (not) thrown exceptions
217 |
218 | You can match on an expression throwing or not throwing an exception:
219 |
220 | something().must.throw_!SomeException;
221 | something().must.not.throw_!SomeOtherException;
222 |
223 | The above works when the expression on the left returns a value. If the return type is void, you must wrap
224 | it in calling():
225 |
226 | calling( somethingReturningVoid() ).must.throw_!SomeOtherException;
227 |
228 | ## Writing custom matchers
229 |
230 | Extending with custom matchers is as simple as defining new functions which take a Matcher as the first
231 | argument. D will rewrite a call to matcher.foo(...) to a call to your function void foo(matcher, ...)
232 |
233 | Look at the matchers supplied in matcher.d for an example on how to write them.
234 |
235 |
236 |
237 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "specd",
3 | "description": "Library for expressing specifications (for unit testing) in D.",
4 | "copyright": "Copyright © 2013, Johan Östling",
5 | "authors": [
6 | "Johan Östling"
7 | ],
8 | "dependencies": {
9 | },
10 | "configurations": [
11 | {
12 | "name": "build",
13 | "targetType": "library"
14 | },
15 | {
16 | "name": "test",
17 | "versions": ["specrunner", "SpecDTests"],
18 | "targetType": "executable",
19 | "buildOptions": ["unittests"]
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/source/specd/matchers.d:
--------------------------------------------------------------------------------
1 | module specd.matchers;
2 |
3 | import specd.specd;
4 |
5 | import std.math;
6 | import std.range;
7 | import std.traits;
8 | import std.stdio;
9 | import std.conv, std.string;
10 |
11 | version(SpecDTests) unittest {
12 | bool evaluated = false;
13 | int oneTimeCalculation() {
14 | evaluated = true;
15 | return 1;
16 | }
17 |
18 | describe("the must function")
19 | .should("wrap a lazy expression in a Match without evaluating it", (when) {
20 | auto m = oneTimeCalculation().must();
21 | assert(evaluated == false);
22 | assert(m.match() == 1);
23 | assert(evaluated == true);
24 | })
25 | ;
26 |
27 | }
28 |
29 | version(SpecDTests) unittest {
30 | import std.array;
31 | import std.format;
32 | auto a = 1;
33 | const(int) f() { return a; }
34 | describe("const(T)")
35 | .should("work with matchers", (_) { f().must.equal(a); });
36 |
37 | string fmt(double d) {
38 | auto w = appender!string();
39 | formattedWrite(w, "%e", d);
40 | return w.data;
41 | }
42 |
43 | double x = 1.0;
44 | double y = 1.0;
45 | double toleranceWeak = 0.00001;
46 | double toleranceStrict = 0.0000001;
47 | double z = 1.0 + 0.000001;
48 | describe(text("x and y (", fmt(x), ",", fmt(y), ")"))
49 | .should("be approxEqual since they are equal", (_) {
50 | x.must.be.approxEqual(y, 0, 0);
51 | x.must.approxEqual(y, 0, 0);
52 | });
53 |
54 | describe(text("x and z (", fmt(x), ",", fmt(z), ")"))
55 | .should("be approxEqual", (_) {
56 | x.must.be.approxEqual(z, toleranceWeak, toleranceWeak);
57 | x.must.approxEqual(z, toleranceWeak, toleranceWeak);
58 | });
59 |
60 | describe(text("at strict threshold x and z (", fmt(x), ",", fmt(z), ")"))
61 | .should("*not* be approxEqual", (_) {
62 | x.must.not.be.approxEqual(z, toleranceStrict, toleranceStrict);
63 | x.must.not.approxEqual(z, toleranceStrict, toleranceStrict);
64 | });
65 |
66 | describe("[x, y, z]")
67 | .should("be approxEqual [z, y, x]", (_) {
68 | auto first = [x, y, z];
69 | auto second = [z, y, x];
70 | first.must.be.approxEqual(second, toleranceWeak, toleranceWeak);
71 | });
72 |
73 | describe("at strict threshold [x, y, z]")
74 | .should("*not* be approxEqual [z, y, x]", (_) {
75 | [x, y, z].must.not.be
76 | .approxEqual([z, y, x], toleranceStrict, toleranceStrict);
77 | });
78 |
79 | describe("[x, x, x]")
80 | .should("*not* be approxEqual [x, x]", (_) {
81 | [x, x, x].must.not.be
82 | .approxEqual([x, x], toleranceWeak, toleranceWeak);
83 | });
84 | }
85 |
86 | version(SpecDTests) unittest {
87 |
88 | describe("equal matching").should([
89 | "work on string": {
90 | "foo".must.equal("foo");
91 | },
92 | "work on int": {
93 | 2.must.equal(2);
94 | },
95 | "work on double": {
96 | 1.3.must.equal(1.3);
97 | },
98 | "work on object": {
99 | auto a = new Object;
100 | a.must.equal(a);
101 | },
102 | "throw a MatchException if it doesn't match": {
103 | try {
104 | 1.must.equal(2);
105 | assert(false, "Expected a MatchException");
106 | } catch (MatchException e) {
107 | }
108 | },
109 | "invert matching with not": {
110 | 2.must.not.equal(1);
111 | }
112 | ]);
113 |
114 | describe("sameAs matching").should([
115 | "work on slices": {
116 | auto a = [1,2,3];
117 | auto b = a;
118 | b.must.be.sameAs(a);
119 | },
120 | "must fail on duplicate arrays": {
121 | auto a = [1,2,3];
122 | auto b = a.dup;
123 | b.must.not.be.sameAs(a);
124 | b.must.equal(a);
125 | },
126 | "work on objects": {
127 | auto a = new Object();
128 | auto b = a;
129 | b.must.be.sameAs(a);
130 | }
131 | ]);
132 |
133 |
134 |
135 | describe("between matching").should([
136 | "match a range": {
137 | 1.must.be.between(1,3);
138 | 2.must.be.between(1,3);
139 | 3.must.be.between(1,3);
140 | 4.must.not.be.between(1,3);
141 | 0.must.not.be.between(1,3);
142 | }
143 | ]);
144 |
145 | describe("contain matching").should([
146 | "match partial strings": {
147 | "frobozz".must.contain("oboz");
148 | "frobozz".must.not.contain("bracken");
149 | }
150 | ]);
151 |
152 | describe("boolean matching").should([
153 | "match on True": {
154 | true.must.be.True;
155 | },
156 | "match on False": {
157 | false.must.be.False;
158 | },
159 | "match using be(true)": {
160 | true.must.be(true);
161 | }
162 | ]);
163 |
164 | describe("null matching").should([
165 | "match on Null": {
166 | Object a = null;
167 | Object b = new Object();
168 | a.must.be.Null;
169 | b.must.not.be.Null;
170 | }
171 | ]);
172 |
173 | describe("opEquals matching").should([
174 | "match == for basic types": {
175 | 1.must == 1;
176 | 1.must.not == 2;
177 | },
178 | "match == for objects": {
179 | Object a = new Object();
180 | Object b = new Object();
181 | a.must == a;
182 | a.must.not == b;
183 | }
184 | ]);
185 |
186 |
187 | describe("comparison matching").should([
188 | "match greater_than": {
189 | 1.must.be.greater_than(0);
190 | 1.must.not.be.greater_than(1);
191 | 1.must.be_!">" (0);
192 | },
193 | "match greater_than_or_equal_to": {
194 | 1.must.be.greater_than_or_equal_to(1);
195 | 1.must.not.be.greater_than_or_equal_to(2);
196 | },
197 | "match less_than": {
198 | 1.must.be.less_than(2);
199 | 1.must.not.be.less_than(1);
200 | },
201 | "match less_than_or_equal_to": {
202 | 1.must.be.less_than_or_equal_to(1);
203 | 1.must.not.be.less_than_or_equal_to(0);
204 | }
205 | ]);
206 |
207 | class TestException : Exception {
208 | this(string s) {
209 | super(s);
210 | }
211 |
212 | }
213 |
214 | int throwAnException() {
215 | throw new TestException("foo");
216 | }
217 |
218 | void throwAnExceptionAndReturnVoid() {
219 | throw new TestException("bar");
220 | }
221 |
222 | describe("Exception matching").should([
223 | "match when an exception is thrown": {
224 | throwAnException().must.throw_!TestException;
225 | 1.must.not.throw_!TestException;
226 | },
227 | "work with void function calls": {
228 | calling(throwAnExceptionAndReturnVoid()).must.throw_!TestException;
229 | }
230 | ]);
231 | }
232 |
233 | private struct Calling {};
234 |
235 | Match!Calling calling(lazy void m, string file = __FILE__, size_t line = __LINE__) {
236 | return new Match!Calling({ m(); return Calling(); }, file, line);
237 | }
238 |
239 | auto must(T)(lazy T m, string file = __FILE__, size_t line = __LINE__) {
240 | static if (is(T : Match!Calling)) {
241 | return m(); // Allow the form calling(foo()).must.throw....
242 | } else {
243 | return new Match!T({ return m(); }, file, line);
244 | }
245 | }
246 |
247 | class Match(T) {
248 | T delegate() match;
249 | // TODO I use this for comparing a generic type with the type of this Match.
250 | // Might be a better way to do that.
251 | T dummyMatch;
252 | // Signal that the match is positive, ie it has not been negated with "not" in the chain
253 | // (or if it has, it has been negated again by a second "not")
254 | bool isPositiveMatch = true;
255 | string file;
256 | size_t line;
257 |
258 | this(T delegate() match, string file, size_t line) {
259 | this.match = match;
260 | this.file = file;
261 | this.line = line;
262 | this.dummyMatch = T.init;
263 | }
264 |
265 | // Negated match
266 |
267 | auto not() {
268 | isPositiveMatch = !isPositiveMatch;
269 | return this;
270 | }
271 |
272 | // Sugar
273 | auto be() {
274 | return this;
275 | }
276 |
277 | // Help for matching booleans
278 | static if (is(T == bool)) {
279 | void be(bool expected) {
280 | if (expected)
281 | True(this);
282 | else
283 | False(this);
284 | }
285 | }
286 |
287 | bool opEquals(T rhs) {
288 | equal(this, rhs);
289 | return true;
290 | }
291 |
292 | static if (isFloatingPoint!T) {
293 | bool approxEqual(T rhs, T maxRelDiff = 1e-2, T maxAbsDiff = 1e-5) {
294 | .approxEqual(this, rhs, maxRelDiff, maxAbsDiff);
295 | return true;
296 | }
297 | } else static if(isInputRange!T && isFloatingPoint!(ElementType!T)) {
298 | bool approxEqual(T rhs, ElementType!T maxRelDiff = 1e-2, ElementType!T maxAbsDiff = 1e-5) {
299 | .approxEqual(this, rhs, maxRelDiff, maxAbsDiff);
300 | return true;
301 | }
302 | }
303 |
304 |
305 | alias Object.opEquals opEquals;
306 |
307 | void throwMatchException(string reason) {
308 | throw new MatchException(reason, file, line);
309 | }
310 | }
311 |
312 |
313 | class MatchException : Exception {
314 | this(string s, string file = __FILE__, size_t line = __LINE__) {
315 | super(s, file, line);
316 | }
317 | }
318 |
319 |
320 | void equal(T, T1)(Match!T matcher, T1 expected)
321 | if (is(typeof(expected == matcher.dummyMatch) == bool))
322 | {
323 | auto match = matcher.match();
324 | if ((expected == match) != matcher.isPositiveMatch)
325 | matcher.throwMatchException("Expected " ~
326 | (matcher.isPositiveMatch ? "" : "not ") ~
327 | "<" ~ text(expected) ~ "> but got <" ~
328 | text(match) ~ ">");
329 | }
330 |
331 |
332 |
333 | void sameAs(T, T1)(Match!T matcher, T1 expected)
334 | if (is(typeof(expected == matcher.dummyMatch) == bool))
335 | {
336 | auto match = matcher.match();
337 | if ((expected is match) != matcher.isPositiveMatch)
338 | matcher.throwMatchException("Expected " ~
339 | (matcher.isPositiveMatch ? "" : "not ") ~
340 | "<" ~ text(expected) ~ "> but got <" ~
341 | text(match) ~ ">");
342 | }
343 |
344 |
345 |
346 | void approxEqual(T, T1, V)(Match!T matcher, T1 expected, V maxRelDiff, V maxAbsDiff)
347 | if (is(typeof(expected == matcher.dummyMatch) == bool))
348 | {
349 | auto match = matcher.match();
350 | if ((std.math.approxEqual(expected, match, maxRelDiff, maxAbsDiff)) != matcher.isPositiveMatch)
351 | matcher.throwMatchException("Expected Approx " ~
352 | (matcher.isPositiveMatch ? "" : "not ") ~
353 | "<" ~ text(expected) ~ "> but got <" ~
354 | text(match) ~ ">");
355 | }
356 |
357 | private string comparisonInWords(string op)() {
358 | if (op == "<")
359 | return "less than";
360 | else if (op == "<=")
361 | return "less than or equal to";
362 | else if (op == ">")
363 | return "greater than";
364 | else if (op == ">=")
365 | return "greater than or equal to";
366 | else
367 | return "*unknown operation*";
368 | }
369 |
370 | private void comparison(string op, T, T1)(Match!T matcher, T1 expected) {
371 | auto match = matcher.match();
372 | auto cmp = mixin("match " ~ op ~ " expected");
373 | if (cmp != matcher.isPositiveMatch)
374 | matcher.throwMatchException("Expected something " ~
375 | (matcher.isPositiveMatch ? "" : "not ") ~
376 | comparisonInWords!op ~
377 | " <" ~ text(expected) ~ "> but got <" ~
378 | text(match) ~ ">");
379 |
380 | }
381 |
382 | void greater_than(T, T1)(Match!T matcher, T1 expected)
383 | if (is(typeof(matcher.dummyMatch > expected) == bool))
384 | {
385 | comparison!">"(matcher, expected);
386 | }
387 |
388 | void greater_than_or_equal_to(T, T1)(Match!T matcher, T1 expected)
389 | if (is(typeof(matcher.dummyMatch >= expected) == bool))
390 | {
391 | comparison!">="(matcher, expected);
392 | }
393 |
394 | void less_than(T, T1)(Match!T matcher, T1 expected)
395 | if (is(typeof(matcher.dummyMatch < expected) == bool))
396 | {
397 | comparison!"<"(matcher, expected);
398 | }
399 |
400 | void less_than_or_equal_to(T, T1)(Match!T matcher, T1 expected)
401 | if (is(typeof(matcher.dummyMatch <= expected) == bool))
402 | {
403 | comparison!"<="(matcher, expected);
404 | }
405 |
406 | void be_(string op, T, T1)(Match!T matcher, T1 expected)
407 | if (is(typeof(matcher.dummyMatch > expected) == bool) &&
408 | (op == ">" || op == ">=" || op == "<" || op == "<="))
409 | {
410 | comparison!op(matcher, expected);
411 | }
412 |
413 | void between(T, T1)(Match!T matcher, T1 first, T1 last)
414 | if (is(typeof(matcher.dummyMatch >= first) == bool))
415 | {
416 | auto match = matcher.match();
417 | bool inrange = (match >= first && match <= last);
418 | if (inrange != matcher.isPositiveMatch)
419 | matcher.throwMatchException("Expected something " ~
420 | (matcher.isPositiveMatch ? "" : "not ") ~
421 | "between <" ~ text(first) ~ "> and <" ~ text(last) ~ "> but got <" ~ text(match) ~ ">");
422 | }
423 | void contain(T, T1)(Match!T matcher, T1 fragment)
424 | if (is(typeof(indexOf(matcher.dummyMatch, fragment) != -1) == bool))
425 | {
426 | auto match = matcher.match();
427 | bool contains = indexOf(match, fragment) != -1;
428 | if (contains != matcher.isPositiveMatch)
429 | matcher.throwMatchException("Expected <" ~ text(match) ~ "> to " ~
430 | (matcher.isPositiveMatch ? "" : "not ") ~
431 | "contain <" ~ text(fragment) ~ ">");
432 | }
433 |
434 | void True(Match!bool matcher) {
435 | auto match = matcher.match();
436 | if (match != matcher.isPositiveMatch)
437 | matcher.throwMatchException("Expected <" ~
438 | (matcher.isPositiveMatch ? "true" : "false") ~
439 | "> but got <" ~
440 | text(match) ~ ">");
441 | }
442 |
443 | void False(Match!bool matcher) {
444 | auto match = matcher.match();
445 | if (match == matcher.isPositiveMatch)
446 | matcher.throwMatchException("Expected <" ~
447 | (matcher.isPositiveMatch ? "false" : "true") ~
448 | "> but got <" ~
449 | text(match) ~ ">");
450 | }
451 |
452 | void Null(T)(Match!T matcher)
453 | if (is(typeof(matcher.dummyMatch is null) == bool))
454 | {
455 | auto match = matcher.match();
456 | bool isNull = match is null;
457 | if (isNull != matcher.isPositiveMatch)
458 | matcher.throwMatchException("Expected " ~
459 | (matcher.isPositiveMatch ? "" : "not ") ~
460 | " but got <" ~
461 | text(match) ~ ">");
462 |
463 | }
464 |
465 | void throw_(E, T)(Match!T matcher)
466 | if (is(E : Throwable))
467 | {
468 | string exception = "";
469 | try {
470 | matcher.match();
471 | if (matcher.isPositiveMatch)
472 | exception = "Expected " ~
473 | E.stringof ~ " thrown, but nothing was thrown";
474 |
475 | } catch (E e) {
476 | if (!matcher.isPositiveMatch)
477 | exception = "Expected no " ~
478 | E.stringof ~ " thrown, but got " ~ typeof(e).stringof;
479 | }
480 |
481 | if (exception != "")
482 | matcher.throwMatchException(exception);
483 | }
484 |
--------------------------------------------------------------------------------
/source/specd/reporter.d:
--------------------------------------------------------------------------------
1 | module specd.reporter;
2 |
3 | import specd.specd;
4 |
5 | import std.stdio;
6 |
7 | interface Reporter {
8 |
9 | void reportSpecificationGroup(SpecificationGroup group);
10 |
11 | void reportSpecification(Specification specification);
12 |
13 | void reportSummary(int totalNumberOfSpecs, int numberOfFailedSpecs);
14 |
15 | final bool report() {
16 | int total = 0;
17 | int failures = 0;
18 | foreach(specGroup; SpecificationGroup.allSpecs) {
19 | reportSpecificationGroup(specGroup);
20 | foreach(spec; specGroup.specifications) {
21 | ++total;
22 | if (!spec.isSuccess) {
23 | ++failures;
24 | }
25 | reportSpecification(spec);
26 | }
27 | }
28 | reportSummary(total, failures);
29 | return failures == 0;
30 | }
31 | }
32 |
33 | class ConsoleReporter : Reporter {
34 | override void reportSpecificationGroup(SpecificationGroup group) {
35 | writeln(mark(group.isSuccess), group.title, " should", markOff());
36 | }
37 |
38 | override void reportSpecification(Specification spec) {
39 | writeln(mark(spec.isSuccess), " ", spec.test, markOff());
40 | if (!spec.isSuccess)
41 | writeln(spec.exception);
42 | }
43 |
44 | override void reportSummary(int totalNumberOfSpecs, int numberOfFailedSpecs) {
45 | auto success = numberOfFailedSpecs == 0;
46 | writeln(mark(success), success ? "SUCCESS" : "FAILURE",
47 | " Failed: ", numberOfFailedSpecs,
48 | " out of ", totalNumberOfSpecs,
49 | markOff());
50 | }
51 |
52 | version(Posix) {
53 | string mark(bool success) {
54 | if (success)
55 | return "\x1b[32m";
56 | else
57 | return "\x1b[31m";
58 | }
59 |
60 | string markOff() {
61 | return "\x1b[39m";
62 | }
63 | } else {
64 | string mark(bool success) { return success ? "[ OK ] " : "[FAIL] "; }
65 | string markOff() { return ""; }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/source/specd/runner.d:
--------------------------------------------------------------------------------
1 | module specd.runner;
2 |
3 | import specd.reporter;
4 |
5 | version(specrunner) {
6 | int main() {
7 | auto reporter = new ConsoleReporter();
8 |
9 | bool completeSuccess = reporter.report();
10 | if (completeSuccess) {
11 | return 0;
12 | } else {
13 | return 10; // Indicate failures so scripts can check result of running unit tests
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/source/specd/specd.d:
--------------------------------------------------------------------------------
1 | module specd.specd;
2 |
3 | public import specd.matchers;
4 | public import specd.runner;
5 | public import specd.specification;
6 |
7 |
--------------------------------------------------------------------------------
/source/specd/specification.d:
--------------------------------------------------------------------------------
1 | module specd.specification;
2 |
3 | import specd.matchers;
4 |
5 | import std.stdio;
6 |
7 | auto describe(string title) {
8 | return new SpecificationGroup(title);
9 | }
10 |
11 | version(SpecDTests) unittest {
12 |
13 | int executionSequence = 0;
14 | int executionFlag = 0;
15 | bool executionRan = false;
16 |
17 | describe("A SpecificationGroup with ordered parts").as(
18 | (it) { it.should("execute each part", (executionSequence++).must.equal(0)); },
19 | (it) { it.should("execute its parts in order", (executionSequence++).must.equal(1)); }
20 | );
21 |
22 | describe("A SpecificationGroup with unordered parts").should([
23 | "execute each part": {
24 | executionFlag |= 1;
25 | },
26 | "execute its parts in any order": {
27 | executionFlag |= 2;
28 | }
29 | ])
30 | ;
31 |
32 | assert(executionFlag == 3, "Did not execute all parts of the unordered SpecificationGroup");
33 |
34 | describe("A SpecificationGroup with a single part")
35 | .should("execute the part", executionRan = true);
36 |
37 | assert(executionRan, "Did not execute the single specification");
38 | }
39 |
40 | class Specification {
41 | package:
42 | string test;
43 | MatchException exception;
44 |
45 | this(string test) {
46 | this.test = test;
47 | this.exception = null;
48 | }
49 |
50 | this(string test, MatchException exception) {
51 | this.test = test;
52 | this.exception = exception;
53 | }
54 | public:
55 | @property bool isSuccess() { return exception is null; }
56 | }
57 |
58 | class SpecificationGroup {
59 | package:
60 | alias Block = void delegate();
61 | alias ItBlock = void delegate(SpecificationGroup);
62 |
63 | static SpecificationGroup[] allSpecs;
64 |
65 | string title;
66 | Specification[] results;
67 |
68 | this(string title) {
69 | this.title = title;
70 | allSpecs ~= this;
71 | }
72 |
73 | public:
74 | @property bool isSuccess() {
75 | foreach(result; results) {
76 | if (!result.isSuccess)
77 | return false;
78 | }
79 | return true;
80 | }
81 |
82 | @property Specification[] specifications() { return results; }
83 |
84 | void as(ItBlock[] parts ...) {
85 | foreach(part; parts) {
86 | part(this);
87 | }
88 | }
89 |
90 | auto should(Block[string] parts) {
91 | foreach (key, value; parts) {
92 | try {
93 | value();
94 | results ~= new Specification(key);
95 | } catch (MatchException e) {
96 | results ~= new Specification(key, e);
97 | }
98 | }
99 | return this;
100 | }
101 | auto should(string text, lazy void test) {
102 | try {
103 | test();
104 | results ~= new Specification(text);
105 | } catch (MatchException e) {
106 | results ~= new Specification(text, e);
107 | }
108 | return this;
109 | }
110 | auto should(string text, ItBlock test) {
111 | try {
112 | test(this);
113 | results ~= new Specification(text);
114 | } catch (MatchException e) {
115 | results ~= new Specification(text, e);
116 | }
117 | return this;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------