0.3.0-alpha.4
requires a recent nightly (2018-08-31 or newer):
$ rustup update
futures-test
With futures 0.3.0-alpha.4
we’re releasing a new independent crate called
futures-test
(under the name futures-test-preview
on crates.io).
The crate contains various testing utilities that were previously only used
internally in the futures crate. We polished them and made them more ergonomic
to use and are now releasing them as a public standalone crate. What’s a little
different compared to the other crates that comprise futures-rs is that this
one is meant to be used as a dev dependency when implementing custom futures.
So, unlike the other crates, its content is not reexported from the futures
facade.
Testing without futures-test
Before getting into the details of futures-test
, it’s useful to go over a
basic technique for testing asynchronous code that may not be known to everyone.
A lot of the time you don’t care about all the details of how some async
operation executes, just that it performs a certain task. In these cases you
can simply use an async block run via futures::executor::block_on
and
have all your assertions in there. Here’s a quick example:
async fn some_operation<F: FnMut(usize)>(callback: F) -> io::Result<usize> {
await!(async { /* Some IO stuff */ });
callback(5);
await!(async { /* Some more IO */ });
callback(6);
await!(async { /* ... */ });
return 7;
}
#[test]
fn test_some_operation() {
futures::executor::block_on(async {
let mut values = vec![];
let result = await!(some_operation(|value| values.push(value)));
assert_eq!(result, Ok(7);
assert_eq!(values, vec![5, 6]);
})
}
Test Context
s
While you can get a long way by just defining and running async tests via
block_on
, sometimes you need to be able to inspect what is happening more
precisely. E.g. when testing FutureExt::fuse
we want to check that after
it completes, poll
correctly returns Poll::Pending
on subsequent calls.
However, to call poll
we need to somehow acquire a task::Context
.
futures-test
makes this easy. It provides you with all sorts of useful contexts
depending on what you are testing. You have two choices to make when you want to
create a context:
- What should happen when a future calls
cx.waker().wake()
? - What should happen when a future calls
cx.spawner().spawn()
?
For both of these there are two very basic implementations provided: panicking
and noop (short for “no operation”). Sometimes this is all you need. For example
when testing FutureExt::fuse
we use futures_test::task::panic_context
which gives a context that will panic on either operation:
#![feature(pin, arbitrary_self_types, futures_api)]
use futures::future::{self, FutureExt};
use futures_test::task::panic_context;
#[test]
fn fuse() {
let mut future = future::ready::<i32>(2).fuse();
let cx = &mut panic_context();
assert!(future.poll_unpin(cx).is_ready());
assert!(future.poll_unpin(cx).is_pending());
}
WakeCounter
One slightly more specialized Wake
implementation is to count how
many times a task has been woken. This has been used to check that
AbortHandle
correctly awakens a waiting task when it is aborted. You can
see from this example that setting up a context with this implementation is
slightly more work. We start with a basic panic_context
, then swap out the
waker with one provided by the WakeCounter
:
#![feature(pin, arbitrary_self_types, futures_api)]
use futures::channel::oneshot;
use futures::future::{abortable, Aborted, FutureExt};
use futures::task::Poll;
use futures_test::task::{panic_context, WakeCounter};
#[test]
fn abortable_awakens() {
let (_tx, a_rx) = oneshot::channel::<()>();
let (mut abortable_rx, abort_handle) = abortable(a_rx);
let wake_counter = WakeCounter::new();
let mut cx = panic_context();
let cx = &mut cx.with_waker(wake_counter.local_waker());
assert_eq!(0, wake_counter.count());
assert_eq!(Poll::Pending, abortable_rx.poll_unpin(cx));
assert_eq!(0, wake_counter.count());
abort_handle.abort();
assert_eq!(1, wake_counter.count());
assert_eq!(Poll::Ready(Err(Aborted)), abortable_rx.poll_unpin(cx));
}
There are some other implementations provided, take a look in the
futures_test::task
module docs for more info.
FutureTestExt
Along with the context utilities there are some more test specific combinators
provided. These come on an additional FutureTestExt
trait. For now there are
three provided:
assert_unmoved
- When writing custom futures that wrap other futures you want to be sure that they’re not accidentally moved after they were polled because that’s not allowed. This combinator can create a future that can detect such accidental moves.
pending_once
- When testing, a lot of the time you will be working with instantly ready
futures like
future::ready(5)
orasync { 6 }
. Frequently you need to have a future that actually acts like a future. This combinator will returnPoll::Pending
once and instantly wake its task to emulate a real async operation. run_in_background
- Sometimes you just want to run some future to completion and not care about the details. This will spin up a dedicated executor on a background thread to run it to completion for you.
assert_stream_*!
The final testing utility is trio of macros for working with streams. Together, these macros allow you to poll a stream one poll at a time and ensure it behaves exactly as you imagined:
#![feature(async_await, futures_api, pin)]
use futures::stream;
use futures_test::future::FutureTestExt;
use futures_test::{
assert_stream_pending, assert_stream_next, assert_stream_done,
};
use pin_utils::pin_mut;
let mut stream = stream::once((async { 5 }).pending_once());
pin_mut!(stream);
assert_stream_pending!(stream);
assert_stream_next!(stream, 5);
assert_stream_done!(stream);
Improvements to come
The work on this crate is not done. We already have some ideas for other useful testing utilities. If you have existing utilities that you’re using in your project or ideas about what would be useful to you please let us know.
Further changes
A complete list of changes can be found in our changelog.