kex docs Standard Library

Mock

type Reader

Mock: deterministic stand-ins for the world outside the program: the filesystem, environment, and console. Networking mocks live under Mock.Net.

These are STATEFUL: Mock.FS.File(path, content) writes into a store the real FS.File then reads back, so a test can write and read again, and clear() undoes it. That state is global and lives until cleared, which is what makes hook ordering and write/read round trips testable, and also what makes two tests able to interfere.

When a test only needs canned ANSWERS, replacing the capability is the better tool: with FS.File = MyFake { ... } do ... end swaps the implementation for one lexical region, holds no global state, needs no clearing, and cannot leak into another test. See spec/capability_stdlib_fs.kex for the shape of a stand-in (kexhq/kex#143).

Opt-in on purpose (issue #144): this module used to ride along inside a prelude networking file, so importing the prelude made every Mock.* reachable from every program without anyone asking for it. Reachable is still not callable: the runtime denies the mock intrinsics outside spec files, the REPL, and --allow-mocks, but it should also not be in scope by accident. A qualified Mock.FS.File(...) auto-loads this file like any other opt-in module.

All Mock.* sub-modules live in a single module Mock block so merged compilation units never see duplicate top-level Mock modules. A stand-in's read hook: a path in, its content or None out.

type Lookup

An environment stand-in's lookup hook: a name in, its value or None out.

type Writer

An environment stand-in's WRITE hook: the name and the value a program set. A Mock.Env is an immutable record, so a write cannot land in its vars, and it must not reach the real environment, or a test would leak a variable into the next one and into anything the suite starts. The hook is how a test observes the write instead: record it, assert on it, or ignore it, which is what None does.

module Mock

record Files

A stand-in for the FS.File capability, for with FS.File = .... Unlike the Mock.* functions below it holds no global state, needs no clear(), and cannot leak past its block. What it cannot do is change: write then read back is not something a value does, so a test needing that round trip still wants Mock.FS (kexhq/kex#143).

with FS.File = Mock.Files { files: {"kex.toml": "name = \"demo\""} } do
  assert(loadConfig() == "demo")
end

onRead takes over when a test needs an ANSWER rather than a fixture: content derived from the path, a failure on the third call, a record of what was asked for. It is consulted first and its None means "no such file", so a callback can model absence too.

Fields

files
Map<FS.FilePath, String> optional
onRead
Reader? optional

Implements FS.File.

Methods

cannedRead

cannedRead(path)

Named apart from read: this.read(path) would bind to the capability's own read : FilePath -> String?, not to this method.

read

read(path)

readBytes

readBytes(path)

readLines

readLines(path)

feed

feed(path)

size

size(path)

exists?

exists?(path)

file?

file?(path)

directory?

directory?(path)

absolute

absolute(path)

open

open(path, mode)

A fake is a value, so there is nowhere for a write to go. Refusing is the honest answer and the useful one: a test that did not expect a write sees it fail rather than silently succeed.

write

write(path, content)

writeBytes

writeBytes(path, content)

append

append(path, content)

delete

delete(path)

copy

copy(src, dst)

rename

rename(src, dst)

record Env

A stand-in for the ENV capability. A name simply left out of vars reads as unset, which is the whole reason Mock.ENV.unset exists: absence is an answer programs act on. onGet answers instead of the map when a test wants a rule rather than a fixture.

with ENV = Mock.Env { vars: {"HOME": "/fake"} } do
  assert(configHome() == "/fake/.config")
end

Fields

vars
Map<String, String> optional
onGet
Lookup? optional
onSet
Writer? optional

Implements ENV.

Methods

lookup

lookup(key)

Named apart from get: this.get(key) would bind to the capability's own get, not to this method.

get

get(key, default)

has?

has?(key)

keys

keys

values

values

count

count

each

each(f)

entries

entries

set

set(name, value)

A write goes to onSet or nowhere. It must NOT reach the real environment: a substituted ENV is the whole point of the mock, and a test that set a variable would otherwise leak it into the next one and into every process the suite starts. vars cannot take it either: a record is immutable, so a test that cares about writes supplies the hook, and one that does not gets a write that goes quietly nowhere.

unset

unset(name)

module Mock.FS

A stateful stand-in for the filesystem: files a test declares, that the real FS.File then reads back.

The store is global and lives until clear, which is what makes a write followed by a read testable, and what makes two tests able to interfere. Clear it in an after hook.

describe "the config loader" do
  before do
    Mock.FS.files({ "app.conf": "port = 8080\n" })
  end

  after do
    Mock.FS.clear()
  end

  it "reads the port" do
    Assert.equal(loadPort(), 8080)
  end
end

File

File(path: FS.FilePath, content: String) -> Void

Declares one file and its content.

Parameters
path
the path the file appears at
content
its contents
Examples
Mock.FS.File("app.conf", "port = 8080\n")

Directory

Directory(path: FS.FilePath) -> Void

Declares a directory at path.

Parameters
path
the directory to declare
Examples
Mock.FS.Directory("src")

clear

clear : Void

Empties the store, so nothing declared so far is visible any more.

Call it in an after hook: the store is global, and what one test leaves behind the next one sees.

Examples
after do
  Mock.FS.clear()
end

files

files(entries: Map<FS.FilePath, String>) -> Void

Declares the whole fixture in one call.

The same shape Mock.Files { files: ... } takes: one line instead of one per file (kexhq/kex#143).

Parameters
entries
paths to their contents
Examples
Mock.FS.files({
  "app.conf": "port = 8080\n",
  "hosts":    "localhost\n"
})

onRead

onRead(reader: Reader) -> Void

Answers reads by RULE rather than from a fixture.

Content derived from the path, a failure on the third call, a record of what was asked for. Consulted before the map, and returning None means "no such file", so absence is expressible too.

Parameters
reader
the rule to answer reads with
Examples

Every .txt path has content, nothing else exists

Mock.FS.onRead do |path|
  path.endsWith?(".txt") then Just("stub") else None
end

module Mock.ENV

Overlays the process environment, so a test can say what ENV holds instead of depending on how it was launched.

The overlay is global and lives until clear: clear it in an after hook.

Mock.ENV.vars({ "HOME": "/fake", "LOG_LEVEL": "debug" })
assert(configPath() == "/fake/.config")
Mock.ENV.clear()

set

set(name: String, value: String) -> Void

Sets one variable in the overlay.

Parameters
name
the variable name
value
its value
Examples
Mock.ENV.set("LOG_LEVEL", "debug")

unset

unset(name: String) -> Void

Removes one variable from the overlay, so it reads as unset.

Separate from set because a variable being ABSENT is an answer programs act on, and there is no value that means it.

Parameters
name
the variable to remove
Examples

Testing the unset path

Mock.ENV.unset("HOME")
Assert.equal(configPath(), ".")

clear

clear : Void

Removes the whole overlay, restoring the real environment.

Examples
after do
  Mock.ENV.clear()
end

vars

vars(entries: Map<String, String>) -> Void

Declares the whole overlay in one call.

The same shape Mock.Env { vars: ... } takes (kexhq/kex#143). There is no onGet here: global ENV is a materialised Map, so there is nothing for a callback to intercept: use with ENV = Mock.Env { onGet: ... } when a rule is what you want.

Parameters
entries
variable names to their values
Examples
Mock.ENV.vars({ "HOME": "/fake", "LOG_LEVEL": "debug" })

module Mock.IO

A stateful stand-in for the console: captures what a program prints, and feeds it lines as if they had been typed.

The way to test a program that talks to a person without one being there.

Mock.IO.start()
Mock.IO.input("Ada", "42")
greet()
Assert.equal(Mock.IO.output(), "hello, Ada\n")
Mock.IO.stop()

start

start : Void

Starts capturing output and serving queued input.

Examples
before do
  Mock.IO.start()
end

stop

stop : Void

Stops capturing, and restores the real console.

Examples
after do
  Mock.IO.stop()
end

output

output : String

Everything the program has printed since capturing started.

Newlines are included, so a single IO.printLine("hi") gives "hi\n".

Returns the captured output

Examples
Mock.IO.start()
IO.printLine("hello")
Assert.equal(Mock.IO.output(), "hello\n")

clear

clear : Void

Discards the captured output, while continuing to capture.

Useful between phases of one test, when only the later output matters.

Examples
setUpNoisily()
Mock.IO.clear()
theThingUnderTest()
Assert.equal(Mock.IO.output(), "done\n")

input

input(first, second, third, fourth) -> Void

Queues the lines IO.getLine will return, in order.

Takes a list, or up to four lines as separate arguments. Once they run out, IO.getLine answers None: end of input, exactly as a closed stdin would.

Parameters
lines
the lines to serve, in order
Examples

A list of lines

Mock.IO.input(["Ada", "42"])

The same, as separate arguments

Mock.IO.input("Ada", "42")

module Mock.Net

Scriptable networking values are namespaced so importing Mock does not recreate any of the removed global HTTP types.

module Mock.Net.HTTP

Canned HTTP transport state for networking specifications.

record Transport

Responses returned in order by a scripted transport.

Fields

responses
[Any]

module Mock.Net.DNS

Canned DNS resolver state for networking specifications.

record ResolverScript

Hostname-to-address answers supplied without touching the network.

Fields

answers
{String: [String]}

module Mock.Net.Socket

Canned byte-stream state for socket specifications.

record Script

Binary chunks delivered in order as incoming socket data.

Fields

incoming
[Binary]

module Mock.Net.WebSocket

Canned message state for WebSocket specifications.

record Script

High-level messages delivered in order by a scripted connection.

Fields

incoming
[Any]