ChaiBdd
About
This class implements most of the functions of the Chai BDD assertion library that make sense in the context of 4D, plus some extra assertions that are specific to 4D. Assertions which the Chai docs say are not recommended were not implemented here.
Normally you will never instantiate this class directly, it is done for you when you call .expect()
.
Usage
For a basic idea of how to use .expect()
assertions, see the Chai documentation. Although the syntax is obviously different, and some features in Chai have not been implemented in js.component, there are useful guidelines for the correct and incorrect usage of each assertion which has not been reproduced here, so it’s worth reading.
Within js.component, the basic syntax for an assertion is this:
This.expect(<target> {; <message>})<chain words><assertion>
For example, let’s say you want to test that a call to the method foo
returns the exact string "bar":
This.expect(foo()).to.strict.equal("bar")
You could also add an assertion that the result is a string (even though .equal()
will fail if it is not), just to make the test more explicit:
This.expect(foo()).to.be.a("string").which.strict.equals("bar")
For an exhaustive example of how to use this class, take a look at the source for the classes in the "test" Explorer folder of this component.
Custom failure messages
It is often useful to group multiple .expect()
clauses in a single test, which allows you to have less verbose test output. In addition, you can chain multiple assertions after a single .expect()
. While this is great when all of the assertions pass, if one or more of the assertions fails, it can be difficult to know which assertion failed.
To avoid this problem, you can specify a custom failure message prefix, both as the message parameter to .expect()
and in most cases as the message parameter to an assertion. If the message parameter is passed and is non-empty, and an assertion fails, the assertion failure message is prefixed with message + ": ".
Caveats
Note there are a few small differences between Chai’s implementation and this implementation.
When testing for equality among strings, by default non-strict equality (case/diacritical-insensitive) is used, like in 4D. Adding
.strict
earlier in the chain will cause case/diacritical-sensitive matching to be used. The.strict
flag is cleared after each assertion in the chain.When using the
.equal()
assertion with objects and collections, deep comparisons are always done, so in effect Chai’s.deep
is implicit, and in fact is not implemented.Because the
.include()
assertion cannot be used both as a function and as a computed property, as it is in Chai, instead of using.to.include.members()
to test for a subset, you must use.to.have.subset()
.
Language chains
The following getters return This
(an instance of ChaiBdd
), allowing you to chain them together arbitrarily to make your assertions read more naturally. Chain words have no idea of grammar or order, so it is up to you to use them in a way that makes sense.
.also
.and
.at
.to
.be
.been
.but
.does
.has
.have
.is
.that
.which
.same
.still
.with
For example, if you want to test that a value is a collection with a length of 3 that contains "foo", this will work, but it isn’t very readable:
This.expect(foo).a("collection").length(3).includes("foo")
Using chain words, you can make it read more naturally:
This.expect(foo).to.be.a("collection").which.has.length(3).and.includes("foo")
Flag words
The following computed properties set a flag affecting the next assertion and return This
for chaining.
.all
.all : cs.js.ChaiBdd
Causes the next .keys()
assertions in the chain to require that the target have all of the given keys. This is the opposite of .any
, which only requires that the target have at least one of the given keys. This flag is cleared after each assertion.
This.expect(New object("a"; 1; "b"; 2)).to.have.all.keys("a"; "b")
Note that .all
is used by default when neither .all
nor .any
are added earlier in the chain. However, it’s often best to add .all
anyway because it improves readability.
See the .keys()
doc for guidance on when to use .any
or .all
.
.any
.any : cs.js.ChaiBdd
Causes the next .keys()
assertion in the chain to only have at least one of the given keys. This is the opposite of .all
, which requires that the target have all of the given keys. This flag is cleared after each assertion.
This.expect(New object("a"; 1; "b"; 2)).to.have.any.keys("a"; "c")
See the .keys()
doc for guidance on when to use .any
or .all
.
.not
.not : cs.js.ChaiBdd
Negates all assertions that follow in the chain up to and including the next assertion, after which this flag is cleared.
This.expect(New object("a"; 1; "b"; 2))\
.to.not.have.property("c")
This.expect(foo()).to.not.throw
Just because you can negate any assertion with .not
doesn’t mean you should. With great power comes great responsibility. It’s often best to assert that the one expected output was produced, rather than asserting that one of countless unexpected outputs wasn’t produced. See individual assertions for specific guidance.
This.expect(2).to.equal(2) // Recommended
This.expect(2).to.not.equal(1) // Not recommended
.ordered
.ordered : cs.js.ChaiBdd
Causes the next .members()
assertion in the chain to require that members be in the same order. This flag is cleared after each assertion.
$collection:=New collection(1; 2)
This.expect($collection).to.have.ordered.members($collection)\
.but.not.to.have.ordered.members($collection.reverse())
When .include()
and .ordered
are combined, the ordering begins at the start of both collections.
$target:=New collection(1; 2; 3)
$assert:=New collection(1; 2)
$other:=New collection(2; 3)
This.expect($target).to.include.ordered.members($assert)
.but.not.include.ordered.members($other);
.strict
.strict : cs.js.ChaiBdd
Causes the next .equal()
or .members()
assertion in the chain to use strict (case/diacritical-sensitive) comparisons for strings.
This.expect("foo").to.equal("Foo").but.not.to.strict.equal("Foo")
Assertions
All assertions except for .fail()
return This
for chaining.
.an()
.an(type : any /* Text | 4D.Class */ { ; message : Text}) : cs.js.ChaiBdd
Asserts that the target’s type is equal to type. Type comparison is done using .valueIs()
.
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
Aliases
.a
.empty()
.empty({ message : Text}) : cs.js.ChaiBdd
Tests “emptiness” of the target. A value is considered empty when:
Type | Condition |
---|---|
Text | length() = 0 |
Collection | .length = 0 |
Object | OB Is empty() = true |
Blob | Blob size = 0 |
4D.Blob | .size = 0 |
Picture | Picture size() = 0 |
4D.EntitySelection | .length = 0 |
4D.File | .size = 0 |
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
.equal()
.equal(value { ; message : Text}) : cs.js.ChaiBdd
Asserts that the target equals value. Equality is tested with .equal()
. If the strict flag was in the chain between the previous assertion and this assertion, the strict parameter to _.equal()
is true.
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
Aliases
.eq
, .equals
.fail()
.fail({ message : Text})
Forces a failure with an optional message. If message is empty, it defaults to "fail!".
IMPORTANT
This assertion must be the last in the chain.
Aliases
.error
.false
.false : cs.js.ChaiBdd
Asserts that the target has the value false.
.include()
.include(value : Text { ; message : Text}) : cs.js.ChaiBdd
.include(value : Object { ; message : Text}) : cs.js.ChaiBdd
When the target is a string, .include()
asserts that the given string value is a substring of the target.
This.expect("foobar").to.include("foo")
When the target is a collection, .include()
asserts that the given value is a member of the target.
This.expect(New collection(1; 2; 3)).to.include(2);
When the target is an object, .include()
asserts that the given object value’s properties are a subset of the target’s properties.
$target:=New object("a"; 1; "b"; 2; "c"; 3)
$value:=New object("a"; 1; "b"; 2)
This.expect($target).to.include($value)
Because .include()
does different things based on the target’s type, it’s important to check the target’s type before using .include()
. See .an()
for info on testing a target’s type.
$value:=New collection(1; 2; 3)
This.expect($value).to.be.a("collection").that.includes(2)
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
Caveats
A few notes on comparisons:
By default any string comparisons are not strict. Add the
.strict
flag to perform strict (i.e. case/diacritical-sensitive) comparisons. This affects strings within collections and objects as well.Comparisons of collections and objects are deep.
Aliases
.includes
, .contain
, .contains
.instanceOf()
.instanceOf(value : Object { ; message : Text}) : cs.js.ChaiBdd
Asserts that the target is an instance of value, which may be:
- A
4D.Class
, in which case value itself is tested against the target - An
Object
, in which case its class is tested against the target
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
This.expect(File("foo/bar")).to.be.instanceOf(4D.File)
$file:=File("foo")
This.expect(File("bar")).to.be.instanceOf($file)
$person:=cs.Person.new("Laurent")
This.expect($person).to.be.instanceOf(cs.Person)
Aliases
.instanceof
.keys()
.keys(key : Text { ; …keyN : Text }) : cs.js.ChaiBdd
.keys(keys : Collection) : cs.js.ChaiBdd
.keys(keyObject : Object) : cs.js.ChaiBdd
Asserts that the target object or collection has the given keys.
When the target is an object, keys can be provided as one or more string parameters, a single collection parameter containing all strings, or a single object parameter. In the latter case, only the keys in the given object matter; the values are ignored. In all cases, matching is strict, since object keys are case/diacritical-sensitive.
$target:=New object("a"; 1; "b"; 2)
This.expect($target).to.have.all.keys("a", "b")
This.expect($target)\
.to.have.all.keys(New collection("a"; "b"))
// ignore values in the .keys parameter
expect($target).to.have.all.keys(New object("a"; 4; "b"; 5))
When the target is a collection, keys are zero-based indexes into the collection and can be provided as one or more integer parameters or a single collection parameter containing all integers.
$target:=New collection("x", "y"])
This.expect($target).to.have.all.keys(0, 1)
This.expect($target).to.have.all.keys(New collection(0, 1))
Because .keys()
does different things based on the target’s type, it’s important to check the target’s type before using .keys()
. See .an()
for info on testing a target’s type.
$target:=New object("a"; 1; "b"; 2)
This.expect($target).to.be.an("object").that.has.all.keys("a", "b")
Aliases
.key
.lengthOf()
.lengthOf(expectedLength : Integer { ; message : Text }) : cs.js.ChaiBdd
Asserts that the target’s length or size is equal to the given number expectedLength. When the target logically has a size rather than a length, it is better semantically to use the alias .size()
or .sizeOf()
.
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
The following target types are supported:
Type | Test |
---|---|
Text | Length($target) |
Collection | $target.length |
Object | $target.size or $target.length is a number |
Blob | BLOB size($target) |
Picture | Picture size($target) |
This.expect("abc").to.have.lengthOf(3)
This.expect(New collection(1; 2; 3)).to.have.lengthOf(3)
This.expect(New object("foo"; "bar"; "length"; 3; "size"; "small"))\
.to.have.lengthOf(3)
// Note: .lengthOf works here, but semantically
// the alias .sizeOf would read better.
This.expect(New object("foo"; "bar"; "length"; "short"; "size"; 3))\
.to.have.lengthOf(3)
This.expect(New object("foo"; "bar"; "length"; "short"; "size"; 3))\
.to.have.sizeOf(3)
TEXT TO BLOB("😃"; $blob; UTF8 text without length)
This.expect($blob).to.have.sizeOf(4)
This.expect($picture).to.have.sizeOf(1024)
Aliases
.length
, .sizeOf
, .size
.match()
.match(pattern : Text { ; message : Text }) : cs.js.ChaiBdd
Asserts that the target matches the regular expression pattern.
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
This.expect("foobar").to.match("(^foo|bar$)")\
.but.not.to.match("(foo$|^bar)")
.members()
.members(members : Collection { ; message : Text }) : cs.js.ChaiBdd
Asserts that the target collection has the exact same members as members.
$target:=New collection(1; 2; 3)
$members:=New collection(2; 1; 3)
This.expect($target).to.have.members($members)
$target:=New collection(1; 2; 2)
$members:=New collection(2; 1; 2)
This.expect($target).to.have.members($members);
By default, member comparisons are not strict. Add .strict
earlier in the chain to perform strict comparisons.
$target:=New collection("foo"; "bar"; "baz")
$members:=New collection("Bar"; "baz"; "foo")
This.expect($target).to.have.members($members)\
.but.not.strict.members($members)
By default, order doesn’t matter. Add .ordered
earlier in the chain to require that members appear in the same order.
$target:=New collection(1; 2; 3)
$members:=New collection(2; 1; 3)
This.expect($target).to.have.ordered.members($target)\
.but.not.have.ordered.members($members)
TIP
.strict.equal()
is effectively the same as .strict.ordered.members()
.
Both collections must be the same size. To compare the target against a subset, use the .subset()
assertion.
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
.null
.null : cs.js.ChaiBdd
Asserts that the target is the value null.
.oneOf()
.oneOf(collection : Collection { ; message : Text }) : cs.js.ChaiBdd
Asserts that the target is a member of collection. Note however that it’s often best to assert that the target is equal to a specific value.
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
This.expect(1).to.equal(1) // Recommended
This.expect(1).to.be.oneOf(New collection(1; 2; 3)) // Not recommended
Comparisons are not strict by default. To use strict comparisons, add .strict
to the chain.
This.expect("foo").to.be.oneOf(New collection("Foo"; "Bar"))
This.expect("foo").to.not.be.strict.oneOf(New collection("Foo"; "Bar"))
.property()
.property(name : Text { ; value : any { ; message : Text }}) : cs.js.ChaiBdd
Asserts that the target has a property with the given name. The target must be an object. Since object keys are case/diacritical-sensitive, name must match a property name exactly.
This.expect(New object("a"; 1)).to.have.property("a")
This.expect(New object("a"; 1)).to.not.have.property("A")
When value is provided, .property()
also asserts that the property’s value is equal to value. By default, matching is not strict. To perform strict matching of value, add .strict
to the chain.
This.expect(New object("foo"; "bar")).to.have.property("foo", "Bar")
This.expect(New object("foo"; "bar")).to.not.have.strict.property("foo"; "Bar")
NOTE
.property()
changes the target of any assertions that follow in the chain to be the value of the property from the original target object.
$target:=New object("a"; 1)
This.expect($target).to.have.property("a").that.is.a("number").which.equals(1)
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ". If you are not comparing with value, however, it’s best to pass message as a parameter to .expect()
.
.respondTo()
.respondTo(name : Text { ; message : Text }) : cs.js.ChaiBdd
Asserts that the target object has a property with the given name which is a function.
$object:=New object("foo"; Formula("bar"))
This.expect($object).to.respondTo("foo")
// Bar class
Function foo() : Text
return "foo"
// Elsewhere…
$bar:=cs.Bar.new()
This.expect($bar).to.respondTo("foo")
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
NOTE
Computed properties cannot be tested with this assertion.
Aliases
.respondsTo
.satisfy()
.satisfy(matcher : Callable { ; message : Text }) : cs.js.ChaiBdd
Asserts that matcher returns a truthy value when passed the target. Truthiness is tested with _.truthy
. Within matcher, This
is null. If you need This
to refer to an object, use _.bind
.
$matcher:=Formula($1.length=$1.distinct().length)
This.expect(New collection(1; 2)).to.satisfy($matcher)
This.expect(New collection(1; 2; 2)).to.not.satisfy($matcher)
The standard js.component error handler is pushed/popped when calling matcher, so you do not have to worry about errors generated by matcher, for example if an incompatible type is passed in. If an error does occur, the assertion fails.
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
.shared
.shared : cs.js.ChaiBdd
Asserts that the target is a shared object or collection.
.string()
.string(str : Text { ; message : Text }) : cs.js.ChaiBdd
Asserts that the target string contains the given substring str.
This.expect("foobar").to.have.string("bar")
Add .not
earlier in the chain to negate .string()
.
This.expect("foobar").to.not.have.string("taco")
By default, matching is not strict. To perform strict matching, add .strict
earlier in the chain.
This.expect("foobar").to.have.strict.string("bar")\
.but.not.to.have.strict.string("Bar")
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
.subset()
.subset(members : Collection { ; message : Text }) : cs.js.ChaiBdd
Asserts that members is a subset of the target collection.
$target:=New collection(1; 2; 3)
This.expect($target).to.have.subset(New collection(1; 2))
Note that duplicates in members are ignored when not comparing order.
$target:=New collection(1; 2; 3)
This.expect($target).to.have.subset(New collection(1; 2; 2; 1))
When comparing order, note that duplicates are removed from later in members.
$target:=New collection(1; 2; 3)
$members:=New collection(1; 2; 1; 2)
// This:
This.expect($target).to.have.ordered.subset($members)
// is equivalent to:
This.expect($target).to.have.ordered.subset(New collection(1; 2))
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
.throw()
.throw() : cs.js.ChaiBdd
.throw(pattern : Text { ; message : Text }) : cs.js.ChaiBdd
.throw(errorCode : Integer { ; message : Text }) : cs.js.ChaiBdd
.throw(class : 4D.Class { ; pattern : Text { ; message : Text }}) : cs.js.ChaiBdd
.throw(error : Error { ; pattern : Text { ; message : Text }}) : cs.js.ChaiBdd
Asserts that the target is a function and that calling it throws an error, optionally matching some other criteria as well.
When no parameters are passed, .throw()
just asserts that an error is thrown.
$target:=Formula(_.throw("This is a test"))
This.expect($target).to.throw()
In the second form, .throw()
invokes the target function and asserts that an error is thrown with a message that matches pattern. If pattern begins and ends with "/" as has at least one character in between, the text between is treated as a regular expression pattern and Match regex
is used for matching. Otherwise .throw()
does a case/diacritical-insensitive contains match.
$target:=Formula(_.throw("This is a test"))
This.expect($target).to.throw("a test")
This.expect($target).to.throw("/^This is.+/")
In the third form, .throw()
invokes the target function and asserts that an error is thrown with an error code that matches errorCode.
$target:=Formula(_.throw("This is a test"; 13))
This.expect($target).to.throw(13)
In the fourth form, .throw()
invokes the target function and asserts that an error is thrown whose class is class. The match must be exact; a subclass of class will not match. If the classes match and pattern is not empty, the error message is matched as in the second form.
$error:=cs.js.DBError.new("This is a test"; -13; ds.test)
$target:=Formula(_.throw($error))
This.expect($target).to.throw(cs.js.DBError; "/^This is.+/")
In the fifth form, .throw()
invokes the target function and asserts that an error is thrown which matches the object error using _.strictEqualObjects
. Context-specific properties are ignored: .method
, .line
, .code
, and .stack
. If the objects match and pattern is not empty, the error message is matched as in the second form.
$error:=cs.js.DBError.new("This is a test"; -13; ds.test)
$target:=Formula(_.throw($error))
$compare:=cs.js.DBError.new("This is a test"; 13; ds.test)
This.expect($target).to.throw($compare; "/^This is.+/")
// => assertion fails, .error property does not match
$compare:=cs.js.DBError.new("This is a test"; -13; ds.test)
This.expect($target).to.throw($compare; "/^This is.+/")
// => assertion passes
.throw
changes the target of any assertions that follow in the chain to be the error object that’s thrown.
$error:=cs.js.DBError.new("This is a test"; -13; ds.test)
$target:=Formula(_.throw($error))
This.expect($target).to.throw(cs.js.DBError)\
.with.property("table"; ds.test)
If message is not empty and the assertion fails, the failure message will be prefixed by message + ": ".
Aliases
.throws
.true
.true : cs.js.ChaiBdd
Asserts that the target has the value true.
.undefined
.undefined : cs.js.ChaiBdd
Asserts that the Value type
of the target is Is undefined
.