kex docs Standard Library 0.4.0-alpha kex.run ↗

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.

Variants

  • abstract

type Lookup

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

Variants

  • abstract

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.

Variants

  • abstract

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

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

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

function File

Declares one file and its content.

File(path, content) : FS.FilePath -> String -> Void
Parameters
path FS.FilePath
the path the file appears at
content String
its contents

Returns: Void

Examples
Mock.FS.File("app.conf", "port = 8080\n")

function Directory

Declares a directory at path.

Directory(path) : FS.FilePath -> Void
Parameters
path FS.FilePath
the directory to declare

Returns: Void

Examples
Mock.FS.Directory("src")

function clear

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.

clear() : Void

Returns: Void

Examples
after do
  Mock.FS.clear()
end

function files

Declares the whole fixture in one call.

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

files(entries) : Map<FS.FilePath, String> -> Void
Parameters
entries Map<FS.FilePath, String>
paths to their contents

Returns: Void

Examples
Mock.FS.files({
  "app.conf": "port = 8080\n",
  "hosts":    "localhost\n"
})

function onRead

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.

onRead(reader) : Reader -> Void
Parameters
reader Reader
the rule to answer reads with

Returns: Void

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()

function set

Sets one variable in the overlay.

set(name, value) : String -> String -> Void
Parameters
name String
the variable name
value String
its value

Returns: Void

Examples
Mock.ENV.set("LOG_LEVEL", "debug")

function unset

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.

unset(name) : String -> Void
Parameters
name String
the variable to remove

Returns: Void

Examples

Testing the unset path

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

function clear

Removes the whole overlay, restoring the real environment.

clear() : Void

Returns: Void

Examples
after do
  Mock.ENV.clear()
end

function vars

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.

vars(entries) : Map<String, String> -> Void
Parameters
entries Map<String, String>
variable names to their values

Returns: Void

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()

function start

Starts capturing output and serving queued input.

start()

Returns: Void

Examples
before do
  Mock.IO.start()
end

function stop

Stops capturing, and restores the real console.

stop()

Returns: Void

Examples
after do
  Mock.IO.stop()
end

function output

Everything the program has printed since capturing started.

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

output()

Returns: String — the captured output

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

function clear

Discards the captured output, while continuing to capture.

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

clear()

Returns: Void

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

function input

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.

input(lines)
Parameters
lines [String]
the lines to serve, in order

Returns: Void

Examples

A list of lines

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

The same, as separate arguments

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

make Files implements FS.File

cannedRead

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

cannedRead(path)

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

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.

open(path, mode)

write

write(path, content)

writeBytes

writeBytes(path, content)

append

append(path, content)

delete

delete(path)

copy

copy(src, dst)

rename

rename(src, dst)

make Env implements ENV

lookup

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

lookup(key)

get

get(key)

has?

has?(key)

keys

keys()

values

values()

count

count()

each

each(f)

entries

entries()

set

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.

set(name, value)

unset

unset(name)

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]