├── .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 | --------------------------------------------------------------------------------