1 module dshould; 2 3 import dshould.ShouldType; 4 5 public import dshould.ShouldType : because, should; 6 public import dshould.basic; 7 public import dshould.contain; 8 public import dshould.empty; 9 public import dshould.stringcmp; 10 public import dshould.thrown; 11 12 // dispatch based on type 13 14 /** 15 * The word `.equal` tests its parameter for equality with the left-hand side. 16 * If the parameters are strings, a colored diff is used. 17 */ 18 public void equal(Should, T)(Should should, T value, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 19 if (isInstanceOf!(ShouldType, Should)) 20 { 21 import dshould.prettyprint : prettyprint; 22 import std.conv : to; 23 import std.json : JSONValue; 24 import std.traits : Unqual; 25 26 static if (is(typeof(should.got()) == string) && is(T == string) && !should.hasWord!"not") 27 { 28 dshould.stringcmp.equal(should, value, Fence(), file, line); 29 } 30 else static if (!should.hasWord!"not") 31 { 32 static if ( 33 is(Unqual!T == JSONValue) 34 && is(Unqual!(typeof(should.got())) == JSONValue)) 35 { 36 should.allowOnlyWords!().before!"equal (string)"; 37 should.terminateChain; 38 39 auto got = should.got(); 40 const gotString = got.toPrettyString; 41 const valueString = value.toPrettyString; 42 } 43 else static if ( 44 __traits(compiles, T.init.toString()) 45 && __traits(compiles, typeof(should.got()).init.toString())) 46 { 47 should.allowOnlyWords!().before!"equal (string)"; 48 should.terminateChain; 49 50 auto got = should.got(); 51 const gotString = got.toString().prettyprint; 52 const valueString = value.toString().prettyprint; 53 } 54 else static if ( 55 __traits(compiles, T.init[0].toString()) 56 && __traits(compiles, typeof(should.got()).init[0].toString())) 57 { 58 should.allowOnlyWords!().before!"equal (string)"; 59 should.terminateChain; 60 61 auto got = should.got(); 62 const gotString = got.to!string.prettyprint; 63 const valueString = value.to!string.prettyprint; 64 } 65 66 } 67 68 static if (__traits(compiles, got)) 69 { 70 if (got != value) 71 { 72 stringCmpError(gotString, valueString, false, file, line); 73 } 74 } 75 else 76 { 77 dshould.basic.equal(should, value, Fence(), file, line); 78 } 79 } 80 81 public auto equal(Should)(Should should) 82 if (isInstanceOf!(ShouldType, Should)) 83 { 84 return dshould.basic.equal(should); 85 } 86 87 /// be is equivalent to equal for the .should.be(value) case 88 public void be(Should, T)(Should should, T value, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 89 if (isInstanceOf!(ShouldType, Should)) 90 { 91 static if (should.hasNoWords && !is(T == typeof(null))) 92 { 93 return equal(should, value, Fence(), file, line); 94 } 95 else 96 { 97 // be is basic.be for all other cases 98 return dshould.basic.be(should, value, Fence(), file, line); 99 } 100 } 101 102 // be is basic.be for all other cases 103 public auto be(Should)(Should should) 104 if (isInstanceOf!(ShouldType, Should)) 105 { 106 return dshould.basic.be(should); 107 } 108 109 // be is basic.be for all other cases 110 public auto be(Should, T)( 111 Should should, T expected, ErrorValue error, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 112 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately") 113 { 114 return dshould.basic.be(should, expected, error, Fence(), file, line); 115 } 116 117 /** 118 * The word `.throwA` (or `.throwAn`) expects its left-hand side expression to throw an exception. 119 * Instead of the cumbersome `.where.msg.should.equal("msg")`, the `msg` of the Exception to expect 120 * can be passed directly. 121 */ 122 public template throwA(T : Throwable) 123 { 124 void throwA(Should)(Should should, string msgTest, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 125 if (isInstanceOf!(ShouldType, Should)) 126 { 127 dshould.thrown.throwA!T(should, Fence(), file, line).where.msg.should.equal(msgTest, Fence(), file, line); 128 } 129 130 auto throwA(Should)(Should should, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 131 if (isInstanceOf!(ShouldType, Should)) 132 { 133 return dshould.thrown.throwA!T(should, Fence(), file, line); 134 } 135 } 136 137 /// ditto 138 public alias throwAn = throwA; 139 140 @("because defines reason for assertion") 141 unittest 142 { 143 2.should.be(5).because("string A") 144 .should.throwA!FluentException.where.reason.should.equal("string A"); 145 } 146 147 @("compares messages in throwA string overload") 148 unittest 149 { 150 2.should.be(5).because("string A") 151 .should.throwA!FluentException("string B") 152 .should.throwA!FluentException; 153 } 154 155 @("prints informative errors for int comparison") 156 unittest 157 { 158 2.should.be(3).should.throwA!FluentException("Test failed: expected value == 3, but got 2"); 159 } 160 161 @("prints informative errors for object comparison") 162 unittest 163 { 164 Object obj; 165 166 obj.should.not.be(null) 167 .should.throwA!FluentException("Test failed: expected non-null, but got null"); 168 169 obj = new Object; 170 171 obj.should.be(null) 172 .should.throwA!FluentException("Test failed: expected null, but got object.Object"); 173 174 obj.should.not.be.same.as(obj) 175 .should.throwA!FluentException( 176 "Test failed: expected different reference than object.Object, but got same reference"); 177 178 obj.should.be.same.as(new Object) 179 .should.throwA!FluentException( 180 "Test failed: expected same reference as object.Object, but got object.Object"); 181 } 182 183 @("prints informative errors for inequalities") 184 unittest 185 { 186 2.should.be.greater.equal(5) 187 .should.throwA!FluentException("Test failed: expected value >= 5, but got 2"); 188 189 2.should.not.be.less.equal(5) 190 .should.throwA!FluentException("Test failed: expected value not <= 5, but got 2"); 191 } 192 193 @("prints informative errors for range emptiness") 194 unittest 195 { 196 (int[]).init.should.not.be.empty 197 .should.throwA!FluentException("Test failed: expected nonempty range, but got []"); 198 199 [5].should.be.empty 200 .should.throwA!FluentException("Test failed: expected empty range, but got [5]"); 201 } 202 203 @("prettyprints json values for comparison") 204 unittest 205 { 206 import std.json : parseJSON; 207 208 const expected = `Test failed: expected ` ~ ` 209 { 210 ` ~ red(`- "b": "Bar"`) ~ ` 211 }, but got ` ~ ` 212 { 213 ` ~ green(`+ "a": "Foo"`) ~ ` 214 }`; 215 216 const left = parseJSON(`{"a": "Foo"}`); 217 const right = parseJSON(`{"b": "Bar"}`); 218 219 left.should.equal(right) 220 .should.throwA!FluentException(expected); 221 left.should.be(right) 222 .should.throwA!FluentException(expected); 223 } 224 225 @("prints informative errors for approximate checks") 226 unittest 227 { 228 2.should.approximately.be(4, error=0.5) 229 .should.throwA!FluentException("Test failed: expected 4 ± 0.5, but got 2"); 230 231 (2.4).should.not.approximately.be(2, error=0.5) 232 .should.throwA!FluentException("Test failed: expected value outside 2 ± 0.5, but got 2.4"); 233 } 234 235 @("asserts when forgetting to terminate should expression") 236 unittest 237 { 238 void test() 239 { 240 2.should; 241 } 242 243 test.should.throwAn!Exception("unterminated should-chain!"); 244 } 245 246 @("exceptions in the lhs don't set off the unterminated-chain error") 247 unittest 248 { 249 int foo() 250 { 251 throw new Exception(""); 252 } 253 254 foo.should.be(3).should.throwAn!Exception; 255 } 256 257 @("nullable equality") 258 unittest 259 { 260 import std.typecons : Nullable; 261 262 Nullable!int(42).should.not.equal(Nullable!int()); 263 264 Nullable!int(42).should.equal(Nullable!int()).should.throwA!FluentException 265 ("Test failed: expected Nullable.null, but got 42"); 266 } 267 268 @("nullable equality") 269 unittest 270 { 271 import std.typecons : Nullable; 272 273 Nullable!string().should.not.equal(""); 274 275 Nullable!string().should.equal("").should.throwA!FluentException 276 ("Test failed: expected value == '', but got Nullable!string.null"); 277 } 278 279 @("exception thrown by value is not hijacked by unterminated should-chain error") 280 unittest 281 { 282 int foo() 283 { 284 throw new Exception("foo"); 285 } 286 287 foo.should.equal(2).should.throwAn!Exception("foo"); 288 2.should.equal(foo).should.throwAn!Exception("foo"); 289 } 290 291 @("compare two unequal values with the same toString") 292 unittest 293 { 294 class Class 295 { 296 override bool opEquals(const Object other) const 297 { 298 return false; 299 } 300 301 override string toString() const 302 { 303 return "Class"; 304 } 305 } 306 307 auto first = new Class; 308 auto second = new Class; 309 310 (first == second).should.be(false); 311 first.should.equal(second).should.throwAn!Exception("Test failed: expected Class, but got Class"); 312 }