Earl Assertion Library

website·Indexed 32 days ago

https://earl.fun/

An ergonomic, modern, and type-safe assertion library written in TypeScript. Earl provides a comprehensive suite of validators, matchers, and deep equality algorithms for testing, including support for snapshot testing, Zod schema validation, and custom extensions.

Tokens
10.1K
Snippets
71
Records
78
Agent score
64%

What's inside Earl

  1. Overview of Earl assertion library

    Earl is a modern, type-safe assertion library for the TypeScript ecosystem. It is designed to combine the simplicity and speed of runners like Mocha with a robust, type-safe API for assertions, aiming to replace less type-safe alternatives like Chai.
  2. Create and assert mocks with mockObject

    Use mockObject to substitute real functions or objects with mocks. You only need to define the values you care about; other properties will error on access. Earl provides built-in validators, such as .toHaveBeenOnlyCalledWith(), to verify mock interactions.
    import { expect, mockObject } from 'earl'
    
    interface IApi {
      getUser(id: number): Promise<User>
      getOrders(userId: number): Promise<Order[]>
    }
    
    // Create mock
    const api = mockObject<IApi>({
      getUser: async (id) => ({ id, name: 'John', email: '[email protected]' }),
    })
    
    const user = await api.getUser(1)
    expect(user.name).toEqual('John')
    
    // Assert mock was called with specific arguments
    expect(api.getUser).toHaveBeenOnlyCalledWith(1)
  3. Understand the earl Equality Algorithm

    The equality algorithm is an extensible and parameterized deep equality checker used by all deep equality functions in earl. It determines equality through a three-step process:

    1. Matcher Check: If the second value is a Matcher, equality is based on whether the first value matches it.
    2. Circular Reference Check: If a value is self-referencing (equal to an ancestor), it is considered equal to another value if that value is also equal to its ancestor at the same distance.
    3. Category Comparison: Values are assigned to a category. If categories differ, values are unequal. If they match, they are compared using the specific rules for that category.
  4. Verify asynchronous rejections with toBeRejected

    Use the toBeRejected validator to check if an asynchronous function or a promise is rejected. Because this involves asynchronous code, you must use the await keyword when calling expect.
    async function fetchResource() {
      throw new Error('Failed to fetch resource')
    }
    
    const rejectedPromise = Promise.reject(new Error('Something went wrong'))
    
    await expect(fetchResource()).toBeRejected()
    await expect(rejectedPromise).toBeRejected()
  5. Bootstrap a new TypeScript project for Earl

    To set up a new project from scratch, initialize a package.json with test scripts and a .gitignore to exclude build artifacts and dependencies.
    // package.json
    {
      "name": "my-project",
      "private": true,
      "scripts": {
        "build": "tsc",
        "test": "mocha"
      }
    }
    # .gitignore
    node_modules
    dist
  6. Install Earl assertion library

    Earl is a modern, type-safe assertion library written in TypeScript. It is designed to be used with test runners such as mocha, uvu, or node:test.
    # Using npm
    npm install --save-dev earl
    
    # Using yarn
    yarn add --dev earl
    
    # Using pnpm
    pnpm add --save-dev earl
  7. Check value equality using toEqual, toLooseEqual, and toExactlyEqual

    Earl provides three primary validators for equality depending on the required strictness:

    • toEqual: The standard choice for objects and primitives. It uses a deep equality algorithm, checks prototypes, is type-safe, and supports matchers.
    • toLooseEqual: A non-type-safe version of toEqual that performs deep equality checks but ignores prototypes and types. Supports matchers.
    • toExactlyEqual: Checks for reference equality using strict equality. Recommended primarily for objects, as it behaves identically to toEqual for primitives.
    class Vector {
      constructor(public x: number, public y: number) {}
    }
    
    // Deeply check values, TypeScript types and prototypes
    expect(new Vector(1, 2)).toEqual(new Vector(1, 2));
    
    // Only deeply check values (ignores prototype/type)
    expect(new Vector(1, 2)).toLooseEqual({ x: 1, y: 2 });
    
    // Only check reference equality
    const v = new Vector(1, 2);
    expect(v).toExactlyEqual(v);
  8. Configure TypeScript for Earl

    Install TypeScript and create a tsconfig.json in the project root. A minimal configuration is required to ensure Earl is resolved correctly and output is directed to a dist folder.
    npm install --save-dev typescript
    // tsconfig.json
    {
      "compilerOptions": {
        "lib": ["ES2020"],
        "target": "ES2020",
        "strict": true,
        "moduleResolution": "node",
        "outDir": "dist"
      }
    }
  9. Verify asynchronous rejection reasons with toBeRejectedWith

    Use the toBeRejectedWith validator to verify the specific reason for a promise rejection. Similar to toThrow, it accepts a specific error message (string or regex) or a combination of an error class and a message. This must be used with await.
    async function fetchResource() {
      throw new Error('Failed to fetch resource')
    }
    
    const rejectedPromise = Promise.reject(new Error('Custom error message'))
    
    // Verify using Error class and a regex pattern
    await expect(fetchResource()).toBeRejectedWith(Error, /resource/)
    
    // Verify using a specific error message string
    await expect(rejectedPromise).toBeRejectedWith('Custom error message')
  10. Create mock objects with mockObject

    The mockObject function creates a mock object of a specific TypeScript type. You provide an object containing the properties you wish to mock; any property accessed that was not explicitly mocked will throw an error. Functions defined in the mock object are automatically replaced with mock functions, allowing you to use mockFn customization methods on them.
    import { mockObject } from 'earl'
    
    class Person {
      constructor(public name: string, public age: number) {}
      isAdult() { return this.age >= 18 }
    }
    
    const mock = mockObject<Person>({
      isAdult: () => true,
    })
    
    expect(mock.isAdult()).toEqual(true)
    
    // Reconfigure the mock function behavior
    mock.isAdult.returns(false)
    
    // Accessing unmocked properties throws
    expect(() => mock.name).toThrow('no mock value provided')
  11. Implement snapshot testing with Earl

    Snapshot testing in Earl compares the output of your code against a stored snapshot to ensure large outputs do not change unexpectedly. Because Earl is an assertion library and not a full framework, you must provide the current test context to the toMatchSnapshot method so Earl can identify the running test and select the correct snapshot file.

    Snapshots are stored in .snapshot files located in the same directory as the test file. To update existing snapshots, set the UPDATE_SNAPSHOTS environment variable to true.

  12. Validate values using Zod schemas in Earl

    Earl provides built-in support for Zod schema validation. You can use the toMatchSchema validator to verify a single value against a schema, or the expect.schema() matcher to validate elements within a collection (such as an array) using toEqual.
    import * as z from 'zod'
    import { expect } from 'earl'
    
    const schema = z.object({
      foo: z.string(),
      bar: z.number(),
    })
    
    // Validate a single object
    expect({ foo: 'foo', bar: 1 }).toMatchSchema(schema)
    
    // Validate multiple objects within an array
    expect([
      { foo: 'one', bar: 1 },
      { foo: 'two', bar: 2 },
    ]).toEqual([expect.schema(schema), expect.schema(schema)])