Skip to content

Testing

Using it.effect for Effect-based tests.

import { assert, describe, it } from "@effect/vitest"
import { Effect, Fiber, Schema } from "effect"
import { TestClock } from "effect/testing"
describe("@effect/vitest basics", () => {
it.effect("runs Effect code with assert helpers", () =>
Effect.gen(function*() {
const upper = ["ada", "lin"].map((name) => name.toUpperCase())
assert.deepStrictEqual(upper, ["ADA", "LIN"])
assert.strictEqual(upper.length, 2)
assert.isTrue(upper.includes("ADA"))
}))
it.effect.each([
{ input: " Ada ", expected: "ada" },
{ input: " Lin ", expected: "lin" },
{ input: " Nia ", expected: "nia" }
])("parameterized normalization %#", ({ input, expected }) =>
Effect.gen(function*() {
assert.strictEqual(input.trim().toLowerCase(), expected)
}))
it.effect("controls time with TestClock", () =>
Effect.gen(function*() {
const fiber = yield* Effect.forkChild(
Effect.sleep(60_000).pipe(Effect.as("done" as const))
)
// Move virtual time forward to complete sleeping fibers immediately.
yield* TestClock.adjust(60_000)
const value = yield* Fiber.join(fiber)
assert.strictEqual(value, "done")
}))
it.live("uses real runtime services", () =>
Effect.gen(function*() {
const startedAt = Date.now()
yield* Effect.sleep(1)
assert.isTrue(Date.now() >= startedAt)
}))
// For property-based testing, use `it.effect.prop` with Schema-based
// arbitraries
it.effect.prop("reversing twice is identity", [Schema.String], ([value]) =>
Effect.gen(function*() {
const reversedTwice = value.split("").reverse().reverse().join("")
assert.strictEqual(reversedTwice, value)
}))
})

How to test Effect services that depend on other services.

import { assert, describe, it, layer } from "@effect/vitest"
import { Array, Context, Effect, Layer, Ref } from "effect"
export interface Todo {
readonly id: number
readonly title: string
}
// Create a test ref service that can be used to store and manipulate test data
// in layers.
export class TodoRepoTestRef extends Context.Service<TodoRepoTestRef, Ref.Ref<Array<Todo>>>()("app/TodoRepoTestRef") {
static readonly layer = Layer.effect(TodoRepoTestRef, Ref.make(Array.empty()))
}
class TodoRepo extends Context.Service<TodoRepo, {
create(title: string): Effect.Effect<Todo>
readonly list: Effect.Effect<ReadonlyArray<Todo>>
}>()("app/TodoRepo") {
static readonly layerTest = Layer.effect(
TodoRepo,
Effect.gen(function*() {
const store = yield* TodoRepoTestRef
const create = Effect.fn("TodoRepo.create")(function*(title: string) {
const todos = yield* Ref.get(store)
const todo = { id: todos.length + 1, title }
yield* Ref.set(store, [...todos, todo])
return todo
})
const list = Ref.get(store)
return TodoRepo.of({
create,
list
})
})
).pipe(
// Provide the test ref layer as a dependency for the test repo layer.
// Use Layer.provideMerge so the tests can also access the test ref directly
// if needed.
Layer.provideMerge(TodoRepoTestRef.layer)
)
}
class TodoService extends Context.Service<TodoService, {
addAndCount(title: string): Effect.Effect<number>
readonly titles: Effect.Effect<ReadonlyArray<string>>
}>()("app/TodoService") {
static readonly layerNoDeps = Layer.effect(
TodoService,
Effect.gen(function*() {
const repo = yield* TodoRepo
const addAndCount = Effect.fn("TodoService.addAndCount")(function*(title: string) {
yield* repo.create(title)
const todos = yield* repo.list
return todos.length
})
const titles = repo.list.pipe(
Effect.map((todos) => todos.map((todo) => todo.title))
)
return TodoService.of({
addAndCount,
titles
})
})
)
// You would also add a live layer here that provides real dependencies for
// production code.
//
// static readonly layer = Layer.effect(TodoService, ...).pipe(
// Layer.provide(TodoRepo.layer)
// )
static readonly layerTest = this.layerNoDeps.pipe(
// Provide the test repo layer as a dependency for the test service layer.
// Use `Layer.provideMerge` so the tests can also access the test repo
// directly if needed, as well as the test ref through the repo layer.
Layer.provideMerge(TodoRepo.layerTest)
)
}
// `layer(...)` creates one shared layer for the block and tears it down in
// `afterAll`, so all tests inside can access the same service context.
layer(TodoRepo.layerTest)("TodoRepo", (it) => {
it.effect("tests repository behavior", () =>
Effect.gen(function*() {
const repo = yield* TodoRepo
const before = (yield* repo.list).length
assert.strictEqual(before, 0)
yield* repo.create("Write docs")
const after = (yield* repo.list).length
assert.strictEqual(after, 1)
}))
it.effect("layer is shared", () =>
Effect.gen(function*() {
const repo = yield* TodoRepo
const before = (yield* repo.list).length
assert.strictEqual(before, 1)
yield* repo.create("Write docs again")
// because the layer is shared between tests, the todo created in the
// previous test is still present, so the count should be 2, not 1
const after = (yield* repo.list).length
assert.strictEqual(after, 2)
}))
})
describe("TodoService", () => {
it.effect("tests higher-level service logic", () =>
Effect.gen(function*() {
const ref = yield* TodoRepoTestRef
const service = yield* TodoService
const count = yield* service.addAndCount("Review docs")
const titles = yield* service.titles
assert.isTrue(count >= 1)
assert.isTrue(titles.some((title) => title.includes("Review docs")))
// You can also access the test ref directly to make assertions about the
// underlying data.
const todos = yield* Ref.get(ref)
assert.isTrue(todos.length >= 1)
}).pipe(Effect.provide(TodoService.layerTest)))
})