aboutsummaryrefslogtreecommitdiff
path: root/docs/pages/14-testing.md
blob: fa196b054df2706572b3e291b781d09259b3a215 (plain) (blame)
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>