Skip to content

Commit 63fd7d7

Browse files
committed
Add topic page for testing
1 parent dcd2c8a commit 63fd7d7

File tree

3 files changed

+190
-1
lines changed

3 files changed

+190
-1
lines changed

content/tokio/topics/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ The currently available topics articles are:
1111
* [Graceful shutdown](/tokio/topics/shutdown)
1212
* [Getting started with Tracing](/tokio/topics/tracing)
1313
* [Next steps with Tracing](/tokio/topics/tracing-next-steps)
14+
* [Testing](/tokio/topics/testing)

content/tokio/topics/testing.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
title: "Unit Testing"
3+
---
4+
5+
The purpose of this page is to give advice on how to write useful unit tests in
6+
asynchronous applications.
7+
8+
## Pausing and resuming time in tests
9+
10+
Sometimes, asynchronous code explicitly waits by calling [`tokio::time::sleep`]
11+
or waiting on a [`tokio::time::Interval::tick`]. Testing behaviour based on
12+
time (for example, an exponential backoff) can get cumbersome when the unit
13+
test starts running very slowly. However, internally, the time-related
14+
functionality of tokio supports pausing and resuming time. Pausing time has the
15+
effect that any time-related future may become ready early. The condition for
16+
the time-related future resolving early is that there are no more other futures
17+
which may become ready. This essentially fast-forwards time when the only
18+
future being awaited is time-related:
19+
20+
```rust
21+
#[tokio::test]
22+
async fn paused_time() {
23+
tokio::time::pause();
24+
let start = std::time::Instant::now();
25+
tokio::time::sleep(Duration::from_millis(500)).await;
26+
println!("{:?}ms", start.elapsed().as_millis());
27+
}
28+
```
29+
30+
This code prints `0ms` on a reasonable machine.
31+
32+
For unit tests, it is often useful to run with paused time throughout. This can
33+
be achieved simply by setting the macro argument `start_paused` to `true`:
34+
35+
```rust
36+
#[tokio::test(start_paused = true)]
37+
async fn paused_time() {
38+
let start = std::time::Instant::now();
39+
tokio::time::sleep(Duration::from_millis(500)).await;
40+
println!("{:?}ms", start.elapsed().as_millis());
41+
}
42+
```
43+
44+
See [tokio::test "Configure the runtime to start with time paused"](https://docs.rs/tokio/latest/tokio/attr.test.html#configure-the-runtime-to-start-with-time-paused) for more details.
45+
46+
Of course, the temporal order of future resolution is maintained, even when
47+
using different time-related futures:
48+
49+
```rust
50+
#[tokio::test(start_paused = true)]
51+
async fn interval_with_paused_time() {
52+
let mut interval = interval(Duration::from_millis(300));
53+
let _ = timeout(Duration::from_secs(1), async move {
54+
loop {
55+
interval.tick().await;
56+
println!("Tick!");
57+
}
58+
})
59+
.await;
60+
}
61+
```
62+
63+
This code immediately prints `"Tick!"` exactly 4 times.
64+
65+
[`tokio::time::Interval::tick`]: https://docs.rs/tokio/1/tokio/time/struct.Interval.html#method.tick
66+
[`tokio::time::sleep`]: https://docs.rs/tokio/1/tokio/time/fn.sleep.html
67+
68+
## Mocking using [`AsyncRead`] and [`AsyncWrite`]
69+
70+
The generic traits for reading and writing asynchronously ([`AsyncRead`] and
71+
[`AsyncWrite`]) are implemented by, for example, sockets. They can be used for
72+
mocking I/O performed by a socket.
73+
74+
Consider, for setup, this simple TCP server loop:
75+
76+
```no_run
77+
use tokio::net::TcpListener;
78+
79+
#[tokio::main]
80+
async fn main() {
81+
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
82+
loop {
83+
let Ok((mut socket, _)) = listener.accept().await else {
84+
eprintln!("Failed to accept client");
85+
continue;
86+
};
87+
88+
tokio::spawn(async move {
89+
let (reader, writer) = socket.split();
90+
// Run some client connection handler, for example:
91+
// handle_connection(reader, writer)
92+
// .await
93+
// .expect("Failed to handle connection");
94+
});
95+
}
96+
}
97+
```
98+
99+
Here, each TCP client connection is serviced by its dedicated tokio task. This
100+
task owns a reader and a writer, which are [`split`] off of a [`TcpStream`].
101+
102+
Consider now the actual client handler task, especially the `where`-clause of the
103+
function signature:
104+
105+
```rust
106+
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
107+
108+
async fn handle_connection<Reader, Writer>(
109+
reader: Reader,
110+
mut writer: Writer,
111+
) -> std::io::Result<()>
112+
where
113+
Reader: AsyncRead + Unpin,
114+
Writer: AsyncWrite + Unpin,
115+
{
116+
let mut line = String::new();
117+
let mut reader = BufReader::new(reader);
118+
119+
loop {
120+
if let Ok(bytes_read) = reader.read_line(&mut line).await {
121+
if bytes_read == 0 {
122+
break Ok(());
123+
}
124+
writer
125+
.write_all(format!("Thanks for your message.\r\n").as_bytes())
126+
.await
127+
.unwrap();
128+
}
129+
line.clear();
130+
}
131+
}
132+
```
133+
134+
Essentially, the given reader and writer, which implement [`AsyncRead`] and
135+
[`AsyncWrite`], are serviced sequentially. For each received line, the handler
136+
replies with `"Thanks for your message."`.
137+
138+
To unit test the client connection handler, a [`tokio_test::io::Builder`] can
139+
be used as a mock:
140+
141+
```rust
142+
# use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
143+
#
144+
# async fn handle_connection<Reader, Writer>(
145+
# reader: Reader,
146+
# mut writer: Writer,
147+
# ) -> std::io::Result<()>
148+
# where
149+
# Reader: AsyncRead + Unpin,
150+
# Writer: AsyncWrite + Unpin,
151+
# {
152+
# let mut line = String::new();
153+
# let mut reader = BufReader::new(reader);
154+
#
155+
# loop {
156+
# if let Ok(bytes_read) = reader.read_line(&mut line).await {
157+
# if bytes_read == 0 {
158+
# break Ok(());
159+
# }
160+
# writer
161+
# .write_all(format!("Thanks for your message.\r\n").as_bytes())
162+
# .await
163+
# .unwrap();
164+
# }
165+
# line.clear();
166+
# }
167+
# }
168+
169+
#[tokio::test]
170+
async fn client_handler_replies_politely() {
171+
let reader = tokio_test::io::Builder::new()
172+
.read(b"Hi there\r\n")
173+
.read(b"How are you doing?\r\n")
174+
.build();
175+
let writer = tokio_test::io::Builder::new()
176+
.write(b"Thanks for your message.\r\n")
177+
.write(b"Thanks for your message.\r\n")
178+
.build();
179+
let _ = handle_connection(reader, writer).await;
180+
}
181+
```
182+
183+
[`AsyncRead`]: https://docs.rs/tokio/latest/tokio/io/trait.AsyncRead.html
184+
[`AsyncWrite`]: https://docs.rs/tokio/latest/tokio/io/trait.AsyncWrite.html
185+
[`split`]: https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html#method.split
186+
[`TcpStream`]: https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html
187+
[`tokio_test::io::Builder`]: https://docs.rs/tokio-test/latest/tokio_test/io/struct.Builder.html

doc-test/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ publish = false
1010
async-stream = "0.2"
1111
mini-redis = "0.4"
1212
tokio = { version = "1", features = ["full"] }
13+
tokio-test = "0.4"
1314
tokio-stream = "0.1"
1415
bytes = "1"
1516
futures = "0.3"
@@ -18,7 +19,7 @@ crossbeam = "0.8"
1819
tracing = "0.1.34"
1920
tracing-subscriber = "0.3.11"
2021
console-subscriber = "0.1.6"
21-
opentelemetry = "0.17.0"
22+
opentelemetry = "0.17.0"
2223
tracing-opentelemetry = "0.17.2"
2324
opentelemetry-aws = "0.5.0"
2425
opentelemetry-otlp = "0.10.0"

0 commit comments

Comments
 (0)