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
|
<h1 align="center">Testing Your Program</h1>
<p align="center">
Writing unit tests for Chain and Renderer
</p>
A built-in benefit of the pipeline model is **testability**.
A Chain is just a function that takes input and returns output; a Renderer is just a function that takes input and writes content — no global state magic, testing is straightforward.
## Testing Renderer
Renderer is the easiest to test — call the function, assert the result:
```rust
@@@pack!(ResultName = String);
#[renderer]
fn render_greet(result: ResultName) -> RenderResult {
let mut r = RenderResult::new();
writeln!(r, "Hello, {}!", *result).ok();
r
}
#[test]
fn test_render_name() {
let result = render_name(ResultName::new("Alice".to_string()));
assert_eq!(result.to_string().as_str(), "Hello, Alice!\n");
}
```
Note the Renderer's return type changed to `-> String` — `#[renderer]` auto-converts `RenderResult` to whatever return type you specify (default is `()`). By returning `String`, you can directly assert on the output content.
## Testing Chain
Testing a Chain is slightly more complex because its return value is `Next` (actually `impl Into<ChainProcess<ThisProgram>>`). You'll need the assertion macros provided by the framework:
```rust
@@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process};
@@@dispatcher!("hello", CMDHello => EntryHello);
@@@pack!(ResultName = String);
@@@pack!(ErrorNoName = ());
@@@#[chain]
@@@fn handle_hello(args: EntryHello) -> Next {
@@@ let name = args.inner.first().cloned().unwrap_or_default();
@@@ if name.is_empty() {
@@@ ErrorNoName::default().to_render()
@@@ } else {
@@@ ResultName::new(name).to_render()
@@@ }
@@@}
#[test]
fn test_handle_hello_with_name() {
let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into();
// Asserts this is a render result (not continuing the chain)
assert_render_result!(chain_process);
// Asserts member_id is ResultName
assert_member_id!(chain_process, ResultName);
// Unpacks the inner value
let result_name = unpack_chain_process!(chain_process, ResultName);
assert_eq!(result_name.inner, "Alice");
}
```
What the three test macros do:
| Macro | Function |
| ----------------------- | ----------------------------------------------------------------- |
| `assert_render_result!` | Asserts Chain returned the render path (not continuing the chain) |
| `assert_member_id!` | Asserts the return value's member ID is a certain type |
| `unpack_chain_process!` | Unpacks the original type from ChainProcess |
## Constructing Data with the entry! Macro
If `extra_macros` is enabled, you can use `entry!` to quickly construct an Entry:
```rust
// Features: ["extra_macros"]
@@@use mingling::{assert_member_id, unpack_chain_process};
@@@use mingling::macros::entry;
@@@dispatcher!("hello", CMDHello => EntryHello);
@@@pack!(ResultName = String);
@@@#[chain]
@@@fn handle_hello(args: EntryHello) -> Next {
@@@ let name = args.inner.first().cloned().unwrap_or_default();
@@@ ResultName::new(name).to_render()
@@@}
#[test]
fn test_with_entry_macro() {
// entry! constructs an Entry from string literals
let entry = entry!("--name", "Alice");
let chain_process = handle_hello(entry).into();
let result_name = unpack_chain_process!(chain_process, ResultName);
assert_eq!(result_name.inner, "Alice");
}
```
## Testing Resource Injection
If a Chain uses resources, you need to provide resource instances in the test:
```rust
@@@use mingling::{assert_render_result, unpack_chain_process};
@@@#[derive(Default, Clone)]
@@@struct ResPrefix(String);
@@@dispatcher!("hello", CMDHello => EntryHello);
@@@pack!(ResultGreeting = String);
@@@
#[chain]
fn handle_hello(args: EntryHello, prefix: &ResPrefix) -> Next {
let name = args.inner.first().cloned().unwrap_or_default();
ResultGreeting::new(format!("{}, {}", prefix.0, name)).to_render()
}
#[test]
fn test_handle_with_resource() {
// Resources need to be passed manually in tests
let result = handle_hello(
EntryHello::new(vec!["World".to_string()]),
&ResPrefix("Hello".to_string()),
);
let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram);
assert_eq!(greeting.inner, "Hello, World");
}
```
The pipeline model makes testing simple: each Chain and Renderer is a relatively independent function — construct input, assert output.
<p align="center" style="font-size: 0.85em; color: clear;">
Written by @Weicao-CatilGrass
</p>
|