Skip to content

An assertion library with an "expect" style interface, inspired by Chai's and built for Deno.

License

Notifications You must be signed in to change notification settings

linuxwolf/expecto

Repository files navigation

EXPECTO!

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

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; exports expect and use, as well as the AssertionError class in use. Without any calls to use(), the Expecto returned by expect() is initialized with the core, typing, membership, and promised assertions.
  • mod/mocked.ts (mock) — This exports the (default) mocked assertion mixin that can be applied via use. It also exports mock which is the std/testing/mock implementation it depends on. NOTE that this requires mod/index.ts.

In addition, the following are useful to extend Expecto:

  • mod/mixin.ts — exports helper types and utilities for creating custom mixins.

USING

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"]);
});

Predicates and Prepositions

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

Flags

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

deep flag

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.

not flag

Modifies the succeeding check to be negated.

expect(() => { doStuff() }).to.not.throw();

As with English, two nots before an assertion cancel each other out:

expect(() => { throw new Error() }).to.not.not.throw(); // NOT RECOMMENDED!

Core (std)

equal(expected [, message ]) / equals(expected [, message ]) check

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");

throw([ errorType [, message ] ]) check

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

Typing (std)

exist([ msg ]) / exists([ msg ]) check

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();

undefined([ msg ]) check

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();

null([ msg ]) check

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();

true([ msg ]) check

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();

false([ msg ]) check

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;

NaN([ msg ]) check

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();

typeOf(type [, msg ]) check

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");

instanceOf(instancClass [, msg ]) check

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);

Membership (std)

empty([ message ]) check

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 0
  • Array/Typed Array (e.g., Uint8Array) — checks .length is 0
  • ArrayBuffer — checks .byteLength is 0
  • string — checks .length is 0
  • object — 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

any flag

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.

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.

members([ expected[] [, message ] ]) check

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 on actual

    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 Set actual

    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 array actual

    const someValue = ["foo", "bar"];
    
    ....
    
    expect(someValue).to.have.members(["foo", "bar"]);
  • Object: checks that all of the given members are properties on actual

    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" ]);

own flag

Modifies the succeeding check to expect actual to own the property.

expect({foo: "foo value"}).to.have.own.property("foo");

property(name [, message ]) check

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");

Stringed (std)

substring(sub [, message ]) check

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

startsWith(sub [, message]) check

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

endsWith(sub [, message ]) check

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

Promised (std)

eventually modifier

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 awaiting).

await expect(somePromise).to.eventually.be.a.typeOf("string").which.equal("fulfilled!");

The ordering of the chain is maintained, just deferred.

rejected([ msg ]) check

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 awaiting).

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");

rejectedWith([ errorType [, msg ] ]) check

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 awaiting).

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!

Mocked (mock)

called([ count [, msg ] ]) check

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

calledWith(args [, msg ]) check

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

EXTENDING

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.

About

An assertion library with an "expect" style interface, inspired by Chai's and built for Deno.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published