Step 01: Expect Assertions
In this step, we implement the expect() function with basic matchers like toBe() and toEqual().
Goals
- Implement
expect(value)that returns an assertion object - Add
toBe()matcher for strict equality (usingObject.is) - Add
toEqual()matcher for deep equality
Implementation
src/expect.ts
typescript
import { isEqual } from "ohash";
export function expect<T>(received: T) {
return {
toBe(expected: T): void {
if (!Object.is(received, expected)) {
throw new Error(`Expected ${String(expected)} but received ${String(received)}`);
}
},
toEqual(expected: T): void {
if (!isEqual(received, expected)) {
throw new Error(
`Expected ${JSON.stringify(expected)} but received ${JSON.stringify(received)}`,
);
}
},
};
}src/index.ts
typescript
// ... (test, it, runTests from Step 00)
export { expect } from "./expect.js";Usage Example
typescript
import { test, expect, runTests } from "../src/index.js";
test("toBe should compare primitive values", () => {
expect(1 + 1).toBe(2);
expect("hello").toBe("hello");
expect(true).toBe(true);
});
test("toBe should use Object.is for comparison", () => {
expect(NaN).toBe(NaN);
});
test("toEqual should compare objects by value", () => {
expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });
expect([1, 2, 3]).toEqual([1, 2, 3]);
});
runTests();Output Example
✓ toBe should compare primitive values
✓ toBe should use Object.is for comparison
✓ toEqual should compare objects by value
Tests: 3 passed, 0 failed, 3 totalExported API
| API | Description |
|---|---|
expect(value).toBe(x) | Strict equality using Object.is |
expect(value).toEqual(x) | Deep equality using ohash's isEqual |
How to Run
bash
cd impls/01-expect-assertions
bun run exampleOr:
bash
pnpm example:01Key Concepts
Why Object.is instead of ===?
Object.is handles edge cases better than ===:
Object.is(NaN, NaN)returnstrue(whileNaN === NaNisfalse)Object.is(0, -0)returnsfalse(while0 === -0istrue)
This matches the behavior of Vitest and Jest's toBe matcher.
Deep Equality with toEqual
For comparing objects and arrays, we use isEqual from ohash. This provides robust deep equality comparison that handles:
- Nested objects and arrays
nullandundefinedDate,RegExp,Map,Set- Circular references
Advanced: Adding More Matchers
Now that we have the basic structure, let's try implementing more matchers!
Exercise 1: Implement toBeTruthy() and toBeFalsy()
These matchers check if a value is truthy or falsy in JavaScript.
typescript
toBeTruthy(): void {
if (!received) {
throw new Error(`Expected ${String(received)} to be truthy`);
}
},
toBeFalsy(): void {
if (received) {
throw new Error(`Expected ${String(received)} to be falsy`);
}
},Usage:
typescript
expect(1).toBeTruthy();
expect("hello").toBeTruthy();
expect(0).toBeFalsy();
expect("").toBeFalsy();Exercise 2: Implement toBeNull(), toBeUndefined(), toBeDefined()
typescript
toBeNull(): void {
if (received !== null) {
throw new Error(`Expected ${String(received)} to be null`);
}
},
toBeUndefined(): void {
if (received !== undefined) {
throw new Error(`Expected ${String(received)} to be undefined`);
}
},
toBeDefined(): void {
if (received === undefined) {
throw new Error(`Expected value to be defined`);
}
},Exercise 3: Implement the .not modifier
The .not property returns an object with the same matchers but with inverted logic.
typescript
export function expect<T>(received: T) {
return {
toBe(expected: T): void {
if (!Object.is(received, expected)) {
throw new Error(`Expected ${String(expected)} but received ${String(received)}`);
}
},
// ... other matchers
not: {
toBe(expected: T): void {
if (Object.is(received, expected)) {
throw new Error(`Expected value to not be ${String(expected)}`);
}
},
// ... other negated matchers
},
};
}Usage:
typescript
expect(1).not.toBe(2);
expect("hello").not.toBe("world");
expect(undefined).not.toBeNull();The .not pattern provides readable assertions and is a common feature in testing libraries like Vitest and Jest.