-
Notifications
You must be signed in to change notification settings - Fork 2
/
mockth.gleam
200 lines (172 loc) · 5.6 KB
/
mockth.gleam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/// A module for mocking functions in Erlang modules - wrapper around [meck](https://github.com/eproxus/meck).
import gleam/erlang/atom.{type Atom}
import gleam/erlang/process.{type Pid}
import gleam/dynamic.{type Dynamic}
import gleam/result
import gleam/list
import gleeunit/should
pub type History {
/// Module, function and arguments that the mock module got called with.
History(pid: Pid, module: String, function: String, arguments: List(Dynamic))
}
@external(erlang, "meck", "expect")
fn do_expect(module: Atom, function: Atom, fun: fun) -> Atom
/// Mock a function.
/// The function `fun` should return the value that the function will return when called.
/// ```gleam
/// pub fn expect1_test() {
/// let assert Ok(_) =
/// mockth.expect("gleam@function", "identity", fn(_) { "hello" })
///
/// mockth.validate("gleam@function")
/// |> should.equal(True)
///
/// mockth.mocked()
/// |> should.equal(["gleam@function"])
///
/// function.identity("world")
/// |> should.equal("hello")
/// }
pub fn expect(
module module: String,
function function: String,
with fun: fun,
) -> Result(Bool, String) {
use #(module, function) <- result.then(load_expect_atoms(module, function))
do_expect(module, function, fun)
|> is_ok_atom
}
@external(erlang, "meck", "validate")
fn do_validate(module: Atom) -> Bool
// The function returns true if the mocked module(s) has been used according to its expectations. It returns false if a call has failed in some way. Reasons for failure are wrong number of arguments or non-existing function (undef), wrong arguments (function clause) or unexpected exceptions.
/// Validate the state of the mock module(s).
/// Validation can detect:
/// - When a function was called with the wrong argument types (function_clause)
/// - When an exception was thrown
/// - When an exception was thrown and expected (via meck:exception/2), which still results in true being returned
///
/// Validation cannot detect:
/// - When you didn't call a function
/// - When you called a function with the wrong number of arguments (undef)
/// - When you called an undefined function (undef)
pub fn validate(module: String) -> Bool {
case to_module_atom(module) {
Ok(module) -> do_validate(module)
Error(_) -> False
}
}
@external(erlang, "meck", "unload")
fn do_unload_all() -> List(Atom)
/// The function returns the list of mocked modules that were unloaded in the process.
/// Unloads all mocked modules from memory.
pub fn unload_all() -> List(String) {
do_unload_all()
|> module_atoms_to_strings
}
@external(erlang, "meck", "unload")
fn do_unload(module: Atom) -> Atom
/// Unload a mocked module or a list of mocked modules.
/// This will purge and delete the module(s) from the Erlang virtual machine. If the mocked module(s) replaced an existing module, this module will still be in the Erlang load path and can be loaded manually or when called.
pub fn unload(module: String) -> Result(Bool, String) {
module
|> to_module_atom
|> result.then(fn(module) {
do_unload(module)
|> is_ok_atom
})
}
@external(erlang, "meck", "mocked")
fn do_mocked() -> List(Atom)
/// Returns the currently mocked modules.
pub fn mocked() -> List(String) {
do_mocked()
|> module_atoms_to_strings
}
type Mfa =
#(Atom, Atom, List(Dynamic))
@external(erlang, "meck", "history")
fn do_history(module: Atom) -> List(#(Pid, Mfa, result))
/// Returns the history of calls to the mocked module.
/// Dont support exception handling.
pub fn history(module: String) -> Result(List(History), String) {
module
|> to_module_atom
|> result.map(fn(module) { do_history(module) })
|> result.then(fn(histories) {
histories
|> list.map(fn(histoy) {
let #(pid, mfa, _result) = histoy
let #(module, function, arguments) = mfa
let module = atom.to_string(module)
let function = atom.to_string(function)
History(
pid: pid,
module: module,
function: function,
arguments: arguments,
)
})
|> Ok
})
}
/// Utility function to set up mocks with the `use` sugar.
/// ```gleam
/// pub fn with_mock_test() {
/// use mock <- mockth.with_mock(
/// module: "gleam@function",
/// function: "identity",
/// replacement: fn(_) { "hello" },
/// )
///
/// function.identity("world")
/// |> should.equal("hello")
///
/// // the mocked module is available here as `mocks`
/// // for example to be able to call `mockth.history` with it.
/// }
pub fn with_mock(
module module: String,
function function: String,
replacement fun: a,
testcase f: fn(String) -> b,
) {
let assert Ok(_) = expect(module, function, fun)
validate(module)
|> should.be_true
mocked()
|> list.contains(module)
|> should.be_true
f(module)
unload_all()
}
fn module_atoms_to_strings(module_atoms: List(Atom)) -> List(String) {
module_atoms
|> list.map(atom.to_string)
}
fn is_ok_atom(a: Atom) -> Result(Bool, String) {
let ok = atom.from_string("ok")
case ok {
Ok(ok) ->
case ok == a {
True -> Ok(True)
False -> Error("Failed to expect")
}
Error(_) -> Error("Failed to expect")
}
}
fn load_expect_atoms(
module: String,
function: String,
) -> Result(#(Atom, Atom), String) {
use module <- result.then(to_module_atom(module))
use function <- result.map(to_function_atom(function))
#(module, function)
}
fn to_function_atom(function: String) -> Result(Atom, String) {
atom.from_string(function)
|> result.map_error(fn(_) { "Failed to find function " <> function })
}
fn to_module_atom(module: String) -> Result(Atom, String) {
atom.from_string(module)
|> result.map_error(fn(_) { "Failed to find module " <> module })
}