An assertion library with an "expect" style interface, inspired by Chai's and built for Deno. This is not a one-for-one clone of Chai; rather it is the subset that the authors find most useful with semantics the authors find most intuitive.
It wraps the value under test to provide a collection of properties and methods for chaining assertions.
import { expect } from "https://deno.land/x/[email protected]/mod/index.ts";
Deno.test(() => {
expect(42).to.equal(42);
expect({foo: "foo value"}).to.deep.equal({foo: "foo value"});
});
- INSTALLING
- USING
- EXTENDING
Install like most Deno dependencies, by importing the module(s).
import { expect } from "https://deno.land/x/[email protected]/mod/index.ts";
Although NOT RECOMMENDED, it can be imported unversioned.
import { expect } from "https://deno.land/x/expecto/mod.index.ts";
There are a handful of entrypoints:
mod/index.ts
(std) — This is the standard setup; exportsexpect
anduse
, as well as theAssertionError
class in use. Without any calls touse()
, the Expecto returned byexpect()
is initialized with thecore
,typing
,membership
, andpromised
assertions.mod/mocked.ts
(mock) — This exports the (default)mocked
assertion mixin that can be applied viause
. It also exportsmock
which is the std/testing/mock implementation it depends on. NOTE that this requiresmod/index.ts
.
In addition, the following are useful to extend Expecto:
mod/mixin.ts
— exports helper types and utilities for creating custom mixins.
Assertions are made by calling expect()
with the value under test (actual
) then chaining properties for assertion checks.
Additional checks and properties can be made available with use()
.
use(mocked);
use(customMixin);
Deno.test(() => {
const spied = mocked.spy(nestedFunc);
const result = topFunc();
expect(result).to.customCheck();
expect(spied).to.be.called(1).and.calledWith(["foo", "bar"]);
});
The following predicates only return the current instance of Expecto
; they assist with the readility of assertions.
a
/an
also
and
be
been
does
has
/have
is
of
that
to
which
with
The following "flag" properties are used to modify the assertion that follows them. There are two built-in flags, and mixins can provide others.
Some important notes:
- Not all modifiers are supported by all assertions
- Once an assertion is processed in a chain, all previously-set flags are cleared
Modifies the succeeding check to perform a deep check instead of a strict or shallow check.
expect({foo: "foo value"}).to.deep.equal({foo: "foo value"});
Multiple instances of deep
before a check behave as if it were only one specified.
Modifies the succeeding check to be negated.
expect(() => { doStuff() }).to.not.throw();
As with English, two not
s before an assertion cancel each other out:
expect(() => { throw new Error() }).to.not.not.throw(); // NOT RECOMMENDED!
Compares that actual
strictly equals (===
) the given value.
expect(42).to.equal(42);
expect(someObj).to.equal(anotherObj);
NOTE that equal()
and equals()
have identical behavior; use whichever makes the most grammatical sense.
expect(42).to.equal(42);
expect(someObject).which.equals(anotherObj);
If deep
is applied beforehand, then a comprehensive equality check is performed instead.
expect(someObj).to.deep.equal({foo: "foo value"});
If not
is applied beforehand, then the check is negated (strictly or deeply).
expect(someObj).to.not.equal(anotherObj);
expect(someObj).to.not.deep.equal({bar: "far value"});
A custom message can be provided, which will be used if the check fails.
expect(someObj).to.equal(anotherObj, "objects aren't the same");
expect(someObj).to.not.equal(diffObj, "shouldn't match, but do");
Checks that actual
throws an error when invoked:
expect(() => throw new Error("oops")).to.throw();
A class derived from Error
can be provided as the first argument, to check if the thrown error is an instance of that class.
expect(() => throw new TypeError("bad type")).to.throw(TypeError);
If the check succeeds, the returned Expecto
has the thrown error instance as its actual
, so that further checks can be made on the error.
expect(() => throw new Error("oops")).to.throw().with.property("message").to.have.substring("oops");
A custom message can be provided as the last argument, which is used if the check fails.
expect(() => throw new TypeError("oops")).to.throw(RangeError, "oops");
If not
is applied beforehand, the check is negated (and actual
is unchanged).
expect(() => {}).to.not.throw();
This means, if an error type is provided, the check can succeed if actual
throws a different error type.
expect(() => throw new TypeError("oops")).to.not.throw(RangeError); // NOT RECOMMENDED
If actual
is not a function, a TypeError
is thrown instead of an AssertionError
. This occurs regardless if not
is applied.
expect(42).to.throw(); // throws TypeError
expect(42).to.not.throw(); // still throws TypeError
Checks that actual
exists: is not null
nor undefined
.
expect(someValue).to.exist();
NOTE that equal()
and equals()
have identical behavior; use whichever makes the most grammatical sense.
expect(someValue).to.exist();
expect(somevalue).exists();
A custom message can be provied, which is used if the check fails.
expect(null).to.exist("does not exist!");
If not
is applied beforehand, it negates the check.
expect(null).to.not.exist();
expect(undefined).to.not.exist();
Checks that actual
is undefined
.
expect(someValue).to.be.undefined();
A custom message can be provided, which is used if the check fails.
expect("something").to.be.undefined("is actually defined!");
If not
is applied beforehand, it negates the check.
expect(42).to.not.be.undefined();
expect(null).to.no.be.undefined();
Checks that actual
is null
.
expect(someValue).to.be.null();
A custom message can be provided, which is used if the check fails.
expect("some value").to.be.null("is not null");
If not
is applied beforehand, it negates the check.
expect(42).to.not.be.null();
expect(undefined).to.not.be.null();
Checks that actual
is the boolean true
.
expect(someValue).to.be.true();
It is not enough to be truthy, only true
will pass.
expect(true).to.be.true(); // SUCCEEDS
expect("some value").to.be.true(); // FAILS
A custom message can be provided, which is used if the check fails.
expect(false).to.be.true("isn't true");
expect("some value").to.be.true("isn't true, either");
If not
is applied beforehand, it negates the check.
expect(false).to.not.be.true();
expect("some value").to.not.be.true();
Checks that actual
is the boolean false
.
expect(someValue).to.be.false();
If it not enough to be falsy, only false
will pass.
expect(false).to.be.false(); // SUCCEEDS
expect("").to.be.false(); // FAILS
expect(null).to.be.false(); // FAILS
A custom message can be provided, which is used if the check fails.
expect(true).to.be.false("isn't false");
expect(undefined).to.be.false("isn't false, either");
If not
is applied beforeuand, it negates the check.
expect(true).to.not.be.false;
expect("").to.not.be.false;
Checks that actual
is a number and is NaN
. This check is necessary since NaN !== NaN
in Javascript/Typescript!
expect(someNumber).is.NaN(); // SUCCEEDS if someNumber is NaN
expect(NaN).to.equal(NaN); // ALWAYS FAILS!
The check fails (throws AssertionError
) if actual
is not a number.
expect("some string").is.NaN(); // FAILS with AssertionError
A custom message can be provided, which is used if the check fails.
expect(42).to.be.NaN("is not not-a-number");
If not
is applied beforehand, it negates the check.
expect(42).is.not.NaN();
Checks that actual
is of the given type, where typing
is one of:
bigint
boolean
number
object
string
expect(someValue).is.a.typeOf("string");
A custom message can be provided, which is used if the check fails.
expect(42).is.a.typeOf("string", "not a string!");
If not
is applied beforehand, it negates the check.
expect(42).to.not.be.a.typeOf("string");
Checks that actual
is an instance of the given class.
expect(someValue).is.an.instanceOf(SomeClass);
A custom message can be provided, which will be used if the check fails.
expect(new Date()).is.an.instanceOf(RegExp, "not a Regexp!");
If not
is applied beforehand, it negates the check.
expect(new Date()).to.not.be.an.instanceOf(RegExp);
Checks that actual
is empty.
expect(someArray).is.empty();
expect(someStr).is.empty();
The specific behavior depends on what type actual
is:
Set
/Map
— checks.size
is 0Array
/Typed Array (e.g.,Uint8Array
) — checks.length
is 0ArrayBuffer
— checks.byteLength
is 0string
— checks.length
is 0object
— checks that it has no properties
A custom message can be provided, which will be used if the check fails.
expect(["foo", "bar"]).is.empty("has stuff!");
If not
is applied beforehand, it negates the check.
expect(["foo", "bar"]).is.not.empty();
If actual
does not meet one of the above criteria, a TypeError
is thrown instead of AssertionError
. This occurs regardless if not
is applied.
expect(42).is.empty(); // throws TypeError
expect(42).is.not.empty(); // still throws TypeError
Modifies the succeeding check to only require one of the members to be present, on a membership check.
expect(["foo", "bar"]).to.have.any.members(["foo", "baz", "flag"]);
This flag cancels the all
flag.
Modifies the succeeding check to require all of the members to be present, on a membership check.
expect(["foo", "bar"]).to.have.all.memebers(["foo", "bar"]);
Note that, for members()
, this is its default behavior; it has no effect other than readability.
This flag cancels the any
flag.
Checks that actual
is in possession of all of the given members. The exact behavior depends on the type of actual
:
-
Map<K, V>
: checks that all of the given members are keys onactual
const someValue = new Map(); someValue.set("foo", "foo value"); someValue.set("bar", "bar value"); .... expect(someValue).to.have.members(["foo", "bar"])
-
Set<V>
: checks that all of the given members are contained in Setactual
const someValue = new Set(); someValue.add("foo"); someValue.add("bar"); .... expect(someValue).to.have.members(["foo", "bar"]);
-
Array<V>
: checks that all of the given members are elements in the arrayactual
const someValue = ["foo", "bar"]; .... expect(someValue).to.have.members(["foo", "bar"]);
-
Object
: checks that all of the given members are properties onactual
const someValue = { foo: "foo value", bar: "bar balue", } .... expect(someValue).to.have.members(["foo", "bar"]);
If any
is applied beforehand, it checks that any one of the values is present.
const someValue = ["foo", "bar"];
....
expect(someValue).to.have.any.members(["foo", "baz"]); // SUCCEEDS
expect(someValue).to.have.any.members(["baz", "flag"]); // FAILS
By default a strict comparison is used. If deep
is applied beforehand, a comprehensive equality comparison is used.
const someValue = new Set([
{ foo: "foo value" },
{ bar: "bar value" },
]);
....
expect(someValue).to.have.deep.members([
{ foo: "foo value" },
{ bar: "bar value" },
]);
If not
is applied beforehand, the check is negated.
expect({
foo: "foo value",
bar: "bar value",
}).to.not.have.members([ "car", "par" ]);
Modifies the succeeding check to expect actual
to own the property.
expect({foo: "foo value"}).to.have.own.property("foo");
Checks that actual
is an object which has the given property.
expect(someValue).to.have.property("foo");
If own
is applied beforehand, the check only succeeds if actual
has the property directly and not from its prototype chain.
expect(someValue).to.have.own.property("foo");
If the check succeeds, the returned Expecto
has the property's value as its actual
, so that further checks can be made on the property.
expect(someValue).to.have.property("foo").to.be.a.typeOf("string").which.equals("foo value")
A custom message can be provided, which will be used if the check fails.
expect({foo: "foo value"}).to.have.property("bar", "no bar!!");
If not
is applied beforehand, it negates the check (and actual
is unchanged).
expect({foo: "foo value"}).to.not.have.property("bar");
Checks that actual
is a string that contains the given substring.
expect(someStr).to.have.substring("a string");
A custom message can be provided, which will be used if the check fails.
expect("some string value").to.have.substring("this value", "substring missing");
If not
is applied beforehand, it negates the check.
expect("some string value").to.not.have.substring("this value");
If actual
is not a string, a TypeError
is thrown instead of AssertionError
. This occurs regardless if not
is applied.
expect(42).to.have.substring("42"); // throws TypeError
expect(42).to.not.have.substring("foo"); // still throws TypeError
Checks that actual
is a string that starts with the given substring.
expect(someStr).startsWith("LOG:");
A custom message can be provided, which will be used if the check fails.
expect("some string value").startsWith("LOG:", "not the prefix");
If not
is applied beforehand, it negates the check.
expect("some string value").to.not.startsWith("LOG:");
If actual
is not a string, a TypeError
is thrown instead of AssertionError
. This occurs regardless if not
is applied.
expect(42).startsWith("4"); // throws TypeError
expect(42).to.not.startsWith("2"); // sill throws TypeError
Checks that actual
is a string that ends with the given substring.
expect(someSt).endsWith("suffix");
A custom message can be provided, which will be used if the check fails.
expect("some string value").endsWith("suffix", "missing suffix");
If not
is applied beforehand, it negates the check.
expect("some string value").to.not.endsWith("suffix");
If actual
is not a string, a TypeError
is thrown instead of AssertionError
. This occurs regardless if not
is applied.
expect(42).endsWith("2"); // throws TypeError
expect(42).to.not.endsWith("4"); // still throws TypeError
Treats actual
as a promise and defers all modifiers and checks until that promise is fulfilled.
The returned Expecto
is essentially a thenable Proxy; all the predicates, flags, and checks applied after eventually
are resolved when the Execpto
is resolved (e.g., by await
ing).
await expect(somePromise).to.eventually.be.a.typeOf("string").which.equal("fulfilled!");
The ordering of the chain is maintained, just deferred.
Checks that actual
is a promise that is rejected, asynchronously. Like .eventually
, the Expecto
returned by this check is a thenable Proxy. The check will actually be performed once the promise is fulfilled (e.g., by await
ing).
await expect(somePromsie).to.be.rejected();
A custom message can be provided, which is used if the check fails.
await expect(Promise.reolve(42)).to.be.rejected("was not rejected");
If the check succeeds, the returned Expecto
has the rejection reason as its actual
, so that further checks can be made on the error.
const somePromise = Promise.reject(new Error("oops!"));
....
await expect(somePromise).to.be.rejected().with.property("message").that.has.substring("oops!");
If not
is applied beforehand, it negates the check; actual
is changed the resolved value.
const somePromise = Promise.resolve("some string");
....
await expect(somePromise).to.not.be.rejected().and.is.typeOf("string");
Checks that actual
is a promise that is rejected, asynchronously. Like .eventually
, the Expecto
returned by this check is a thenable Proxy. The check will actually be performed once the promise is fulfilled (e.g., by await
ing).
await expect(somePromise).to.be.rejectedWith();
If the check succeeds, the returned Expecto
has the rejection reason as its actual
, so that further checks can be made on the error.
await expect(somePromise).to.be.rejectedWith().with.property("message").that.has.substring("oops");
A class can be provided as the first argument, to check if the thrown errorr is an instance of that class.
await expect(() => Promise.reject(new TypeError("wrong type"))).to.be.rejectedWith(TypeError);
A custom message can be provided as the last argument, which is used if the check fails.
await expect(() => Promise.reject(new RangeError("out of bounds"))).to.be.rejectedWith(TypeError, "not a type error!");
If not
is applied beforehand, it negates the check.
await expect(Promise.resolve("some string")).to.not.be.rejectedWith();
Note this means the check succeeds if the promise successfully resolved or was rejected with a different error!
Checks that actual
is a Spy or Stub and was called.
expect(someSpy).to.have.been.called();
A count can be provided in the first argument, that checks the spy was called that number of times.
someSpy();
someSpy();
....
expect(someSpy).to.have.been.called(2);
A custom message can be provided as the last argument, which is used if the check fails.
expect(someSpy).to.have.been.called(undefined, "spy never called");
If not
is applied beforehand, it negates the check.
expect(someSpy).to.have.not.been.called();
NOTE the negated check can succeed if a count is provided to called()
and the spy is called a different number of times (e.g., called 5 times but checking for .called(3)
)!
If actual
is not a Spy or Stub, a TypeError
is thrown istead of an AssertionError
. This occurs regardless if not
is applied.
expect(42).to.have.been.called(); // throws TypeError
expect("some string").to.have.not.been.called(); // still throws TypeError
Checks that actual
is a Spy or Stub that was called with the given arguments.
expect(someSpy).to.have.been.calledWith(["foo", "bar"]);
If actual
was called multiple times, this check succeeds if at least one of those calls included the given arguments. By default this check performs a strict (===) equality check over the arguments.
If deep
is applied beforehand, a comprehensive equality check of the arguments is performed.
someSpy({
"foo": "foo value",
"bar": "bar value",
});
expect(someSpy).to.have.been.calledWith([ {"foo": "foo value", "bar": "bar value" }]); // fails
expect(someSpy).to.have.been.deep.calledWith([ {"foo": "foo value", "bar": "bar value" }]); // succeeds
If not
is applied beforehand, it negates the check.
expect(someSpy).to.not.have.been.calledWith(["foo", "bar"]);
If actual
is not a Spy or Stub, a TypeError
is thrown istead of an AssertionError
. This occurs regardless if not
is applied.
expect(42).to.have.been.calledWith([]); // throws TypeError
expect("some string").to.have.not.been.calledWith([]); // still throws TypeError
Expecto can be extended with additional checks and/or flags using the mixin pattern.
Get started by importing mod/mixin.ts
module to access the symbols and utilities for developing your own mixins.
To add a custom mixin to Expecto, implement a factory function that takes the current Expecto-derived class and returns a new Expecto-derived class.
import type { CheckDetails, ExpectoConstructor } from "https://deno.land/x/expecto/mod/mixin.ts";
import { meetsExpectations } from "./custom.ts";
export function customMixin<TargetType, BaseType extends ExpectoConstructor<TargetType>>(Base: BaseType) {
return class CustomMixin extends Base {
constructor(...args: any[]) {
super(...args);
}
cusomCheck(): this {
let result = meetsExpectations(this.actual);
this.check(result, {
positiveOp: "does not meet expectations",
negativeOp: "meets expectations",
} as CheckDetails)
return this;
}
};
}
### Performing Checks
The assertion check is performed using `.check(result: boolean, details: CheckDetails)`; `result` is the result to verify, and `details` provides the following:
* `expected`: `unknown` (*OPTIONAL*) — What `actual` is expected to be
* `positiveOp`: `string` — The operation description if a positive (not `.not`) test fails
* `negativeOp`: `string` — The operation description if a negatved (`.not`) test fails
* `message`: `string` (*OPTIONAL*) — The messge—in its entirety—to use if the test fails
The `.check()` method—by default—tests if `result` is truthy, and throws an `AssertionError` if it is not. If the `not` flag is applied, it instead tests if `result` is falsy, and throws an `AssertionError` if it is not. If `message` is not provided, the rest of `details` is used to construct the error message.
### Helpers
The following protected members are available to mixins to aid in checks:
* `.flags(): string[]` — Retrieves a snapshot of currently-set flags on this Expecto. A flag is set if its name is in the returned array.
* `.hasFlag(flag: string): boolean` — Returns `true` if the given flag is set.
* `.setFlag(flag: string)` — Sets the given flag, including it in the values returned by `flags()`.
* `.unsetFlag(flag: string)`— Removes the given flag, excluding it in the values returned by `flags()`.
* `.toggleFlag(flag: string): boolean` — Toggles the given flag; sets it if it was not before, or unsets it if it was; retursn the current state (`true` for set, `false` otherwise).
* `.create<T>(actual: T): this` — Creates a new Expecto of the same type as this Expecto, using `actual` as the target value and with all the flags currently set on this Expecto.