@zerothrow/vitest
Version:
Vitest matchers for ZeroThrow Result types
311 lines (235 loc) โข 7.98 kB
Markdown
# @zerothrow/vitest
> **๐ง ZeroThrow Layers**
> โข **ZT** โ primitives (`try`, `tryAsync`, `ok`, `err`)
> โข **Result** โ combinators (`map`, `andThen`, `match`)
> โข **ZeroThrow** โ utilities (`collect`, `enhanceAsync`)
> โข **@zerothrow/*** โ ecosystem packages (resilience, jest, etc)
> **ZeroThrow Ecosystem** ยท [Packages โข](https://github.com/zerothrow/zerothrow/blob/main/ECOSYSTEM.md)
[](https://github.com/zerothrow/zerothrow/actions)



<div align="center">
<img src="https://raw.githubusercontent.com/flyingrobots/image-dump/refs/heads/main/optimized/marketing/brand/zerothrow-vitest.webp" height="300" />
</div>
Vitest matchers for ZeroThrow Result types - write expressive tests for Result-based error handling with zero-cost abstractions.
## Installation
```bash
npm install @zerothrow/vitest @zerothrow/core @zerothrow/expect
# or: pnpm add @zerothrow/vitest @zerothrow/core @zerothrow/expect
```
## Quick Start
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['./test/setup.ts']
}
});
// test/setup.ts
import '@zerothrow/vitest';
// Or manually setup:
// import { setup } from '@zerothrow/vitest';
// setup();
```
Now use the matchers in your tests:
```typescript
import { expect, test } from 'vitest';
import { ZT } from '@zerothrow/core';
test('should handle successful operations', () => {
const result = ZT.ok(42);
expect(result).toBeOk();
expect(result).toBeOkWith(42);
});
test('should handle errors', () => {
const result = ZT.err(new Error('Something went wrong'));
expect(result).toBeErr();
expect(result).toBeErrWith({ message: 'Something went wrong' });
});
```
## API
### Matchers
#### `toBeOk()`
Asserts that a Result is Ok (success).
```typescript
const result = ZT.ok('success');
expect(result).toBeOk(); // โ
passes
```
#### `toBeOkWith(expected)`
Asserts that a Result is Ok with a specific value.
```typescript
const result = ZT.ok({ id: 1, name: 'Alice' });
expect(result).toBeOkWith({ id: 1, name: 'Alice' }); // โ
passes
```
#### `toBeErr()`
Asserts that a Result is Err (failure).
```typescript
const result = ZT.err(new Error('Failed'));
expect(result).toBeErr(); // โ
passes
```
#### `toBeErrWith(error)`
Asserts that a Result is Err with specific error properties.
```typescript
const result = ZT.err(new Error('Network error'));
// Match by error instance
expect(result).toBeErrWith(new Error('Network error')); // โ
// Match by properties
expect(result).toBeErrWith({ message: 'Network error' }); // โ
// With error codes (ZeroError)
const codeError = ZT.err('NETWORK_ERROR', 'Connection failed');
expect(codeError).toBeErrWith({
code: 'NETWORK_ERROR',
message: 'Connection failed'
}); // โ
```
#### `toHaveErrorCode(code)`
Asserts that a Result contains an error with a specific code.
```typescript
const result = ZT.err('USER_NOT_FOUND', 'User does not exist');
expect(result).toHaveErrorCode('USER_NOT_FOUND'); // โ
passes
```
#### `toHaveErrorMessage(message)`
Asserts that a Result contains an error with a specific message.
```typescript
const result = ZT.err(new Error('Connection timeout'));
// Exact match
expect(result).toHaveErrorMessage('Connection timeout'); // โ
// RegExp match
expect(result).toHaveErrorMessage(/timeout/i); // โ
```
### TypeScript Support
All matchers are fully typed and extend Vitest's `Assertion` interface:
```typescript
import { Result } from '@zerothrow/core';
declare module 'vitest' {
interface Assertion {
toBeOk(): void;
toBeOkWith<T>(expected: T): void;
toBeErr(): void;
toBeErrWith<E extends Error>(error: E | { code?: string; message?: string }): void;
toHaveErrorCode(code: string): void;
toHaveErrorMessage(message: string | RegExp): void;
}
}
```
## Examples
### Testing async operations with combinators
```typescript
import { expect, test } from 'vitest';
import { ZT } from '@zerothrow/core';
async function fetchUser(id: number) {
return ZT.tryAsync(async () => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
}
test('should transform user data', async () => {
const result = await fetchUser(1)
.then(r => r
.map(user => user.name.toUpperCase())
.tap(name => console.log('Processing:', name))
);
expect(result).toBeOk();
expect(result).toBeOkWith('ALICE');
});
test('should handle errors gracefully', async () => {
const result = await fetchUser(999)
.then(r => r
.mapErr(err => new Error(`User fetch failed: ${err.message}`))
.orElse(() => ZT.ok({ id: 0, name: 'Guest' }))
);
expect(result).toBeOk();
expect(result).toBeOkWith({ id: 0, name: 'Guest' });
});
```
### Testing with ZeroError codes
```typescript
import { expect, test } from 'vitest';
import { ZT } from '@zerothrow/core';
function validateEmail(email: string) {
if (!email.includes('@')) {
return ZT.err('INVALID_EMAIL', 'Email must contain @');
}
return ZT.ok(email.toLowerCase());
}
test('email validation', () => {
const valid = validateEmail('user@example.com');
expect(valid).toBeOk();
expect(valid).toBeOkWith('user@example.com');
const invalid = validateEmail('invalid');
expect(invalid).toBeErr();
expect(invalid).toHaveErrorCode('INVALID_EMAIL');
expect(invalid).toHaveErrorMessage('Email must contain @');
});
```
### Testing Result chains and combinators
```typescript
import { expect, test } from 'vitest';
import { ZT, ZeroThrow } from '@zerothrow/core';
function parseAndDouble(input: string) {
return ZT.try(() => JSON.parse(input))
.andThen(val => {
if (typeof val !== 'number') {
return ZT.err('TYPE_ERROR', 'Expected number');
}
return ZT.ok(val * 2);
});
}
test('should compose multiple transformations', () => {
const result = parseAndDouble('42')
.map(n => n / 2) // Back to original
.map(n => n + 10) // Add 10
.tap(n => expect(n).toBe(52))
.map(n => `Result: ${n}`);
expect(result).toBeOk();
expect(result).toBeOkWith('Result: 52');
});
test('should handle error mapping', () => {
const result = parseAndDouble('invalid')
.tapErr(err => console.error('Parse failed:', err))
.mapErr(err => ({
type: 'PARSE_ERROR',
original: err.message
}));
expect(result).toBeErr();
expect(result).toBeErrWith({
type: 'PARSE_ERROR',
original: expect.stringContaining('Unexpected token')
});
});
test('should provide fallback values', () => {
const result = parseAndDouble('"hello"')
.orElse(() => parseAndDouble('100'));
expect(result).toBeOk();
expect(result).toBeOkWith(200);
});
test('should collect multiple results', async () => {
const inputs = ['42', '100', 'invalid', '50'];
const results = inputs.map(parseAndDouble);
// Get all successes, ignoring errors
const successes = results
.filter(r => r.ok)
.map(r => r.unwrapOr(0));
expect(successes).toEqual([84, 200, 100]);
// Or use ZeroThrow.collect to fail fast
const collected = ZeroThrow.collect(results.slice(0, 2));
expect(collected).toBeOk();
expect(collected).toBeOkWith([84, 200]);
});
```
## Manual Setup
If you prefer not to use automatic setup, you can manually configure the matchers:
```typescript
// test/setup.ts
import { expect } from 'vitest';
import { vitestMatchers } from '@zerothrow/vitest';
expect.extend(vitestMatchers);
```
## Contributing
See the [main repository](https://github.com/zerothrow/zerothrow) for contribution guidelines.
## License
MIT