1.5.1 • Published 9 months ago

great-expectations v1.5.1

Weekly downloads
-
License
MIT
Repository
github
Last release
9 months ago

Great Expectations!

Great Expectations provides a framework for constructing rich descriptions of values. It can be used in most javascript testing frameworks, and it works in both node or the browser. Use Great Expectations with Typescript to get faster feedback on the descriptions you write.

There are two guiding principles behind the design of Great Expectations:

  1. Descriptions should be determinate.

Consider a numeric value. One might be tempted to describe it by asserting that it is not 7 or less than 8. While this narrows down the possibilities, it still leaves the value itself vague. Great Expectations was designed to be used in example- based tests where the test writer should have enough control over the test conditions to know exactly what value to expect. Therefore, Great Expectations does not come with matchers for not or less than etc.

Note that there could be good reasons for using matchers that do not describe a value with determinacy -- property-based testing frameworks or fuzz testing might benefit from those, but, again, Great Expectations was written with example-based testing in mind.

  1. It should be very easy to add new matchers.

In Great Expectations, a matcher is just a function from a value to a MatchResult. The MatchResult contains information about how to describe what was expected in human-readable terms. So, while Great Expectations does come with a basic set of matchers, it should be easy enough to create new matchers that are domain-specific, which can make your test suite easier to read and easier to extend.

Getting Started

npm install --save-dev great-expectations

Great Expectations is an excellent matcher library to use with the coolest test framework around: esbehavior. But, you can also use Great Expectations with other javascript testing frameworks, like mocha, jest, uvu. In the case of mocha and jest, you'll need a small adapter to ensure that test failures are printed in the best way. Check out the samples to see how it's done.

Here's how you would use Great Expectations in an esbehavior test:

import { behavior, example, effect } from "esbehavior"
import { expect, is, equalTo } from "great-expectations"

export default behavior("some behavior", [
  example()
    .description("some cool case")
    .script({
      observe: [
        effect("it does the right thing", () => {
          expect("This is cool!", is(stringContaining("cool")))        
        })
      ]
    })
])

Writing Expectations

expect(actual, MatchEvaluator, description?)

The expect function evaluates an actual value against the provided matcher. It will throw an Error if the actual value does not match, which is generally sufficient to cause a test to fail in most JS testing frameworks.

A MatchEvaluator specifies whether the evaluation will occur synchronously or after a Promise resolves. Use is in most cases, and resolvesTo or rejectsWith in cases where the actual value is a Promise.

Optionally, provide a description that will display with any invalid matches.

MatchEvaluators

is(T | Matcher<T>): MatchEvaluator

If the provided value is a matcher, it evaluates the provided matcher against the actual value synchronously. Otherwise, it checks that the provided value deeply equals the actual value.

expect([1, 2, 3], is(arrayWithItemAt(2, equalTo(3))))

throws(T | Matcher<T>): MatchEvaluator

If the provided value is a matcher, it evaluates the provided matcher against any exception thrown when the actual no-argument function is called. Otherwise, it checks that the provided value deeply equals the actual thrown value. To expect that functions with arguments should throw, wrap the call to the function under test with a no-argument function. Fails if the no-argument function does not throw when called.

expect(() => { myFunc("some argument") }, throws(objectOfType(SpecialError)))

resolvesTo(T | Matcher<T>): MatchEvaluator

If the provided value is a matcher, it evaluates the provided matcher against the promised actual value, when that promise resolves. Otherwise, it checks that the provided value deeply equals the promized actual value. Fails if the promise rejects.

rejectsWith(T | Matcher<T>): MatchEvaluator

If the provided value is a matcher, it evaluates the provided matcher against the value received with that promise rejects. Othewise, it checks that the provided value deeply equals the rejected value. Fails if the promise resolves.

Basic Matchers

equalTo(expected)

Asserts that the actual value is deeply equal to the expected value.

identicalTo(expected)

Asserts that the actual value is the same instance as the expected value (ie ===).

defined()

Asserts that the actual value is defined (i.e. not undefined).

assignedWith(matcher)

Asserts that the actual variable is assigned a value (i.e. not undefined) and that it matches the provided matcher. Use this matcher when the expected value is potentially undefined.

const someVariable: string | undefined = "hello"
expect(someVariable, is(assignedWith(stringContaining("he"))))

satisfying(array of matchers)

Asserts that the actual value matches all of the provided matchers.

expect("something fun", is(satisfying([
  stringContaining("fun"),
  stringContaining("something")
])))

String Matchers

stringWithLength(length)

Asserts that the actual value is a string with the given length.

expect("", is(stringWithLength(0)))

stringContaining(expected, { caseSensitive: true, times: undefined })

Asserts that the actual value is a string that contains the expected value at least one time. The optional options specify whether the match should be case sensitive and whether the string should be expected to occur some specific number of times.

stringMatching(regex)

Asserts that the actual value is a string that matches the provided regular expression.

Array Matchers

arrayWithLength(number)

Asserts that the actual value is an array with the given length

arrayContaining(matcher, { times: undefined })

Asserts that the actual value is an array with at least one element that matches the provided matcher. The optional option specifies whether some specific number of elements should match.

arrayWithItemAt(index, matcher)

Asserts that the actual value is an array with an element at the provided index that matches the provided matcher.

arrayWith(array of matchers, { withAnyOrder: false })

Asserts that the actual value is an array with exactly the elements that match the provided array of matchers in the given order. The optional option specifies whether order matters.

expect([ 1, 2, 3 ], is(arrayWith([
  equalTo(1),
  equalTo(2),
  equalTo(3)
])))

Map Matchers

mapWith(array of MapEntryMatcher)

Asserts that the actual value is a map with all and only entries that match the given MapEntryMatchers.

A MapEntryMatcher is an object that conforms to this interface:

{
  key: Matcher<K>,
  value?: Matcher<V>
}
const map = new Map<string, string>()
map.set("Cool Key", "Let's Party!")
map.set("Another Key", "Let's do something fun!")

expect(map, is(mapWith([
  { key: equalTo("Cool Key") },
  { key: equalTo("Another Key"), value: stringContaining("something fun") }
])))

mapContaining(MapEntryMatcher)

Asserts that the actual map contains an entry that matches the given MapEntryMatcher.

Object Matchers

objectOfType(class)

Asserts that the actual value is an object that instantiates the given class.

objectWithProperty(propertyName, matcher)

Asserts that the actual value is an object with an own property of the given name and a value that matches the provided matcher.

objectWith(matcherObject)

Asserts that the actual value is an object that has all the properties of the provided matcher object and the values of those properties match the matchers associated with each propery in the matcher object. If the actual object has additional properties, it will still match.

expect({ name: "cool dude", age: 288 }, is(objectWith({
  name: stringContaining("cool"),
  age: equalTo(288)
})))

Creating Custom Matchers

A matcher is just a function from a value to a MatchResult, which is either an instance of Valid or Invalid. Here's an example of an even matcher:

function even(): Matcher<number> {
  return (actual) => {
    if (actual % 2 === 0) {
      return new Valid({
        actual: value(actual),
        expected: message`a number that is even`
      })
    } else {
      return new Invalid("The actual number was not even.", {
        actual: problem(actual),
        expected: problem(message`a number that is even`)
      })
    }
  }
}

Then you can just use it like any other matcher:

expect(8, is(even()))

MatchResult

A type that is either Valid or Invalid. MatchResult is a discriminated union based on the type field. So it's possible to handle cases like so:

const matchResult = equalTo(7)(5)
switch (matchResult.type) {
  case "valid":
    // do something with the valid result
  case "invalid":
    // do something with the invalid result
}

new Valid({ actual, expected }): MatchResult

Create a Valid when the actual value matches what's expected. The instance looks like this:

{
  type: "valid"
  values: {
    actual: Value | Problem | Message,
    expected: Value | Problem | Message
  }
}

The actual and expected values are used to present these values when necessary. You can use message to create human readable descriptions, value to indicate valid values, and problem to indicate unexpected values.

new Invalid(description, { actual, expected }): MatchResult

Create an Invalid when the actual value fails to match what's expected. The instance looks like this:

{
  type: "invalid"
  description: string
  values: {
    actual: Value | Problem | Message,
    expected: Value | Problem | Message
  }
}

The actual and expected values are used to present these values when necessary. You can use message to create human readable descriptions, value to indicate valid values, and problem to indicate unexpected values.

Creating Messages

message\template literal``

Produces a message from the given template literal. String expressions will be printed as is; other expressions will be stringified. Use value, problem, list, typeName, and times to format expressions.

const expected = "hello"
message`${typeName(expected)} that contains ${value(expected)} ${times(2)}`
===> a string that contains "hello" exactly 2 times

value(value)

Stringifies a given value for pretty output.

problem(value)

Highlights a value as problematic in the output.

list(array of values)

Stringifies the values into a pretty list for output.

typeName(value)

Formats the type of the value for output.

typeName("hello") ===> "a string"

times(count)

Prints the number of times for output.

times(7) ===> "exactly 7 times"
1.5.1

9 months ago

1.5.0

9 months ago

1.4.0

11 months ago

1.2.0

1 year ago

1.3.0

1 year ago

1.1.1

1 year ago

1.1.2

1 year ago

1.1.0

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago