Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-singers-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/vitest": patch
---

Enhance README with test control methods and examples
296 changes: 283 additions & 13 deletions packages/vitest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ This import enhances the standard `it` function from `vitest` with several power
| `it.scopedLive` | Combines the features of `scoped` and `live`, using a live Effect environment that requires a `Scope`. |
| `it.flakyTest` | Facilitates the execution of tests that might occasionally fail. |

All test methods (`it.effect`, `it.live`, `it.scoped`, `it.scopedLive`) support standard Vitest control methods:
- `skip` - Permanently skip a test
- `skipIf` - Conditionally skip a test based on a condition
- `runIf` - Conditionally run a test based on a condition
- `only` - Run only this test, skipping all others
- `each` - Run a test with multiple test cases
- `fails` - Mark a test as expected to fail

# Writing Tests with `it.effect`

Here's how to use `it.effect` to write your tests:
Expand Down Expand Up @@ -160,11 +168,22 @@ it.effect("run the test with the test environment and the time adjusted", () =>
)
```

## Skipping Tests
## Test Control Methods

All test methods (`it.effect`, `it.live`, `it.scoped`, `it.scopedLive`) support the following control methods that extend standard Vitest functionality:

- `skip` - Permanently skip a test
- `skipIf` - Conditionally skip a test based on a condition
- `runIf` - Conditionally run a test based on a condition
- `only` - Run only this test, skipping all others
- `each` - Run a test with multiple test cases
- `fails` - Mark a test as expected to fail

If you need to temporarily disable a test but don't want to delete or comment out the code, you can use `it.effect.skip`. This is helpful when you're working on other parts of your test suite but want to keep the test for future execution.
### Skipping Tests

**Example** (Skipping a Test)
If you need to temporarily disable a test but don't want to delete or comment out the code, you can use `skip`. This is helpful when you're working on other parts of your test suite but want to keep the test for future execution.

**Example** (Skipping Tests)

```ts
import { it } from "@effect/vitest"
Expand All @@ -176,18 +195,106 @@ function divide(a: number, b: number) {
return Effect.succeed(a / b)
}

// Temporarily skip the test for dividing numbers
// Skip a test using it.effect
it.effect.skip("test failure as Exit", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(divide(4, 0))
expect(result).toStrictEqual(Exit.fail("Cannot divide by zero"))
})
)

// Skip a test using it.live
it.live.skip("live test skipped", () =>
Effect.gen(function* () {
const result = yield* divide(10, 2)
expect(result).toBe(5)
})
)

// Skip a test using it.scoped
it.scoped.skip("scoped test skipped", () =>
Effect.acquireRelease(
Effect.gen(function* () {
const result = yield* divide(10, 2)
expect(result).toBe(5)
return result
}),
() => Effect.void
)
)

// Skip a test using it.scopedLive
it.scopedLive.skip("scopedLive test skipped", () =>
Effect.acquireRelease(
Effect.gen(function* () {
const result = yield* divide(10, 2)
expect(result).toBe(5)
return result
}),
() => Effect.void
)
)
```

### Conditionally Skipping Tests

Use `skipIf` to conditionally skip a test based on a runtime condition. This is useful when you want to skip tests in certain environments or when specific conditions are met.

**Example** (Conditionally Skipping Tests)

```ts
import { it, expect } from "@effect/vitest"
import { Effect } from "effect"

const isCI = process.env.CI === "true"

// Skip the test if running in CI
it.effect.skipIf(isCI)("skip in CI", () =>
Effect.gen(function* () {
expect(process.env.CI).not.toBe("true")
})
)

// Skip the test if a feature flag is disabled
const featureEnabled = false
it.live.skipIf(!featureEnabled)("feature test", () =>
Effect.gen(function* () {
expect(featureEnabled).toBe(true)
})
)
```

### Conditionally Running Tests

Use `runIf` to conditionally run a test based on a runtime condition. This is the inverse of `skipIf` - the test runs only when the condition is `true`.

**Example** (Conditionally Running Tests)

```ts
import { it, expect } from "@effect/vitest"
import { Effect } from "effect"

const isDevelopment = process.env.NODE_ENV === "development"

// Run the test only in development
it.effect.runIf(isDevelopment)("development only test", () =>
Effect.gen(function* () {
expect(process.env.NODE_ENV).toBe("development")
})
)

// Run the test only when a condition is met
const shouldRun = true
it.live.runIf(shouldRun)("conditional test", () =>
Effect.gen(function* () {
expect(shouldRun).toBe(true)
})
)
```

## Running a Single Test
### Running a Single Test

When you're developing or debugging, it's often useful to run a specific test without executing the entire test suite. You can achieve this by using `it.effect.only`, which will run just the selected test and ignore the others.
When you're developing or debugging, it's often useful to run a specific test without executing the entire test suite. You can achieve this by using `only`, which will run just the selected test and ignore the others.

**Example** (Running a Single Test)

Expand All @@ -208,30 +315,117 @@ it.effect.only("test failure as Exit", () =>
expect(result).toStrictEqual(Exit.fail("Cannot divide by zero"))
})
)

// Works with all test methods
it.live.only("only live test", () =>
Effect.gen(function* () {
const result = yield* divide(8, 2)
expect(result).toBe(4)
})
)

it.scoped.only("only scoped test", () =>
Effect.acquireRelease(
Effect.gen(function* () {
const result = yield* divide(8, 2)
expect(result).toBe(4)
return result
}),
() => Effect.void
)
)
```

### Running Tests with Multiple Cases

Use `each` to run a test with multiple test cases. This is useful for parameterized tests where you want to test the same logic with different inputs.

**Example** (Running Tests with Multiple Cases)

```ts
import { it, expect } from "@effect/vitest"
import { Effect } from "effect"

// Test with multiple cases using it.effect
it.effect.each([1, 2, 3])("effect each %s", (n) =>
Effect.gen(function* () {
expect(n).toBeGreaterThan(0)
})
)

// Test with multiple cases using it.live
it.live.each([1, 2, 3])("live each %s", (n) =>
Effect.gen(function* () {
yield* Effect.log(`Testing with value ${n}`)
expect(n).toBeGreaterThan(0)
})
)

// Test with multiple cases using it.scoped
it.scoped.each([1, 2, 3])("scoped each %s", (n) =>
Effect.acquireRelease(
Effect.sync(() => expect(n).toBeGreaterThan(0)),
() => Effect.void
)
)

// Test with multiple cases using it.scopedLive
it.scopedLive.each([1, 2, 3])("scopedLive each %s", (n) =>
Effect.acquireRelease(
Effect.sync(() => expect(n).toBeGreaterThan(0)),
() => Effect.void
)
)

// You can also use objects or arrays of objects
it.effect.each([
{ a: 1, b: 2, expected: 3 },
{ a: 2, b: 3, expected: 5 },
{ a: 5, b: 7, expected: 12 }
])("addition test %#", ({ a, b, expected }) =>
Effect.gen(function* () {
expect(a + b).toBe(expected)
})
)
```

## Expecting Tests to Fail
### Expecting Tests to Fail

When adding new failing tests, you might not be able to fix them right away. Instead of skipping them, you may want to assert it fails, so that when you fix them, you'll know and can re-enable them before it regresses.
When adding new failing tests, you might not be able to fix them right away. Instead of skipping them, you may want to assert they fail, so that when you fix them, you'll know and can re-enable them before they regress.

**Example** (Asserting one test fails)
**Example** (Expecting Tests to Fail)

```ts
import { it } from "@effect/vitest"
import { Effect, Exit } from "effect"

function divide(a: number, b: number): number {
function divide(a: number, b: number) {
if (b === 0) return Effect.fail("Cannot divide by zero")
return Effect.succeed(a / b)
}

// Temporarily assert that the test for dividing by zero fails.
it.effect.fails("dividing by zero special cases", ({ expect }) =>
// Mark a test as expected to fail using it.effect
it.effect.fails("dividing by zero special cases", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(divide(4, 0))
expect(result).toStrictEqual(0)
// This assertion will fail, and that's expected
expect(result).toStrictEqual(Exit.succeed(0))
})
)

// Works with all test methods
it.live.fails("expected to fail", () =>
Effect.gen(function* () {
yield* Effect.fail("This failure is expected")
})
)

it.scoped.fails("scoped test expected to fail", () =>
Effect.acquireRelease(
Effect.fail("This failure is expected"),
() => Effect.void
)
)
```

## Logging
Expand Down Expand Up @@ -268,10 +462,49 @@ it.live("it.live displays a log", () =>
)
```

# Writing Tests with `it.live`

The `it.live` method runs tests with the live Effect environment, meaning it uses real services (like the actual system clock) instead of test doubles. This is useful when you want to test with real-world conditions.

**Syntax**

```ts
import { it } from "@effect/vitest"

it.live("test name", () => EffectContainingAssertions, timeout: number | TestOptions = 5_000)
```

`it.live` provides access to live services, which is particularly useful for testing time-sensitive operations with the real clock.

**Example** (Using `it.live` with Real Services)

```ts
import { it, expect } from "@effect/vitest"
import { Clock, Effect } from "effect"

// This test uses the real system clock
it.live("uses real clock", () =>
Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis
expect(now).toBeGreaterThan(0) // Verify we got a valid timestamp
})
)
```

All control methods (`skip`, `skipIf`, `runIf`, `only`, `each`, `fails`) are available on `it.live`, just like with `it.effect`.

# Writing Tests with `it.scoped`

The `it.scoped` method is used for tests that involve `Effect` programs needing a `Scope`. A `Scope` ensures that any resources your test acquires are managed properly, meaning they will be released when the test completes. This helps prevent resource leaks and guarantees test isolation.

**Syntax**

```ts
import { it } from "@effect/vitest"

it.scoped("test name", () => EffectContainingAssertions, timeout: number | TestOptions = 5_000)
```

**Example** (Using `it.scoped` to Manage Resource Lifecycle)

```ts
Expand Down Expand Up @@ -300,6 +533,43 @@ it.scoped("run with scope", () =>
)
```

All control methods (`skip`, `skipIf`, `runIf`, `only`, `each`, `fails`) are available on `it.scoped`, just like with `it.effect`.

# Writing Tests with `it.scopedLive`

The `it.scopedLive` method combines the features of `scoped` and `live`. It runs tests with the live Effect environment while also providing a `Scope` for resource management.

**Syntax**

```ts
import { it } from "@effect/vitest"

it.scopedLive("test name", () => EffectContainingAssertions, timeout: number | TestOptions = 5_000)
```

**Example** (Using `it.scopedLive`)

```ts
import { it, expect } from "@effect/vitest"
import { Clock, Console, Effect } from "effect"

// This test uses both live services and scoped resources
it.scopedLive("live scoped test", () =>
Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis // Real clock
expect(now).toBeGreaterThan(0) // Verify we got a valid timestamp

// Can also use scoped resources
yield* Effect.acquireRelease(
Effect.sync(() => expect(true).toBe(true)),
() => Effect.void
)
})
)
```

All control methods (`skip`, `skipIf`, `runIf`, `only`, `each`, `fails`) are available on `it.scopedLive`, just like with `it.effect`.

# Writing Tests with `it.flakyTest`

`it.flakyTest` is a utility designed to manage tests that may not succeed consistently on the first attempt. These tests, often referred to as "flaky," can fail due to factors like timing issues, external dependencies, or randomness. `it.flakyTest` allows for retrying these tests until they pass or a specified timeout is reached.
Expand Down