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