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
|
<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);
// Returns String instead of ()
#[renderer]
fn render_name(name: ResultName) -> String {
r_println!("Hello, {}!", *name);
}
#[test]
fn test_render_name() {
let result = render_name(ResultName::new("Alice".to_string()));
assert_eq!(result, "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>
|