Testing streams
Verifying behaviour of Akka Stream sources, flows and sinks can be done using various code patterns and libraries. Here we will discuss testing these elements using:
- simple sources, sinks and flows;
- sources and sinks in combination with
TestProbe
from theakka-testkit
module; - sources and sinks specifically crafted for writing tests from the
akka-stream-testkit
module.
It is important to keep your data processing pipeline as separate sources,
flows and sinks. This makes them easily testable by wiring them up to other
sources or sinks, or some test harnesses that akka-testkit
or
akka-stream-testkit
provide.
Built in sources, sinks and combinators
Testing a custom sink can be as simple as attaching a source that emits elements from a predefined collection, running a constructed test flow and asserting on the results that sink produced. Here is an example of a test for a sink:
final Sink<Integer, CompletionStage<Integer>> sinkUnderTest = Flow.of(Integer.class)
.map(i -> i * 2)
.toMat(Sink.fold(0, (agg, next) -> agg + next), Keep.right());
final CompletionStage<Integer> future = Source.from(Arrays.asList(1, 2, 3, 4))
.runWith(sinkUnderTest, mat);
final Integer result = future.toCompletableFuture().get(3, TimeUnit.SECONDS);
assert(result == 20);
The same strategy can be applied for sources as well. In the next example we
have a source that produces an infinite stream of elements. Such source can be
tested by asserting that first arbitrary number of elements hold some
condition. Here the take
combinator and Sink.seq
are very useful.
final Source<Integer, NotUsed> sourceUnderTest = Source.repeat(1)
.map(i -> i * 2);
final CompletionStage<List<Integer>> future = sourceUnderTest
.take(10)
.runWith(Sink.seq(), mat);
final List<Integer> result = future.toCompletableFuture().get(3, TimeUnit.SECONDS);
assertEquals(result, Collections.nCopies(10, 2));
When testing a flow we need to attach a source and a sink. As both stream ends are under our control, we can choose sources that tests various edge cases of the flow and sinks that ease assertions.
final Flow<Integer, Integer, NotUsed> flowUnderTest = Flow.of(Integer.class)
.takeWhile(i -> i < 5);
final CompletionStage<Integer> future = Source.from(Arrays.asList(1, 2, 3, 4, 5, 6))
.via(flowUnderTest).runWith(Sink.fold(0, (agg, next) -> agg + next), mat);
final Integer result = future.toCompletableFuture().get(3, TimeUnit.SECONDS);
assert(result == 10);
TestKit
Akka Stream offers integration with Actors out of the box. This support can be
used for writing stream tests that use familiar TestProbe
from the
akka-testkit
API.
One of the more straightforward tests would be to materialize stream to a
CompletionStage
and then use PatternsCS.pipe
pattern to pipe the result of that future
to the probe.
final Source<List<Integer>, NotUsed> sourceUnderTest = Source
.from(Arrays.asList(1, 2, 3, 4))
.grouped(2);
final TestProbe probe = new TestProbe(system);
final CompletionStage<List<List<Integer>>> future = sourceUnderTest
.grouped(2)
.runWith(Sink.head(), mat);
akka.pattern.PatternsCS.pipe(future, system.dispatcher()).to(probe.ref());
probe.expectMsg(Duration.create(3, TimeUnit.SECONDS),
Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4))
);
Instead of materializing to a future, we can use a Sink.actorRef
that
sends all incoming elements to the given ActorRef
. Now we can use
assertion methods on TestProbe
and expect elements one by one as they
arrive. We can also assert stream completion by expecting for
onCompleteMessage
which was given to Sink.actorRef
.
final Source<Tick, Cancellable> sourceUnderTest = Source.tick(
FiniteDuration.create(0, TimeUnit.MILLISECONDS),
FiniteDuration.create(200, TimeUnit.MILLISECONDS),
Tick.TOCK);
final TestProbe probe = new TestProbe(system);
final Cancellable cancellable = sourceUnderTest
.to(Sink.actorRef(probe.ref(), Tick.COMPLETED)).run(mat);
probe.expectMsg(Duration.create(3, TimeUnit.SECONDS), Tick.TOCK);
probe.expectNoMsg(Duration.create(100, TimeUnit.MILLISECONDS));
probe.expectMsg(Duration.create(3, TimeUnit.SECONDS), Tick.TOCK);
cancellable.cancel();
probe.expectMsg(Duration.create(3, TimeUnit.SECONDS), Tick.COMPLETED);
Similarly to Sink.actorRef
that provides control over received
elements, we can use Source.actorRef
and have full control over
elements to be sent.
final Sink<Integer, CompletionStage<String>> sinkUnderTest = Flow.of(Integer.class)
.map(i -> i.toString())
.toMat(Sink.fold("", (agg, next) -> agg + next), Keep.right());
final Pair<ActorRef, CompletionStage<String>> refAndCompletionStage =
Source.<Integer>actorRef(8, OverflowStrategy.fail())
.toMat(sinkUnderTest, Keep.both())
.run(mat);
final ActorRef ref = refAndCompletionStage.first();
final CompletionStage<String> future = refAndCompletionStage.second();
ref.tell(1, ActorRef.noSender());
ref.tell(2, ActorRef.noSender());
ref.tell(3, ActorRef.noSender());
ref.tell(new akka.actor.Status.Success("done"), ActorRef.noSender());
final String result = future.toCompletableFuture().get(1, TimeUnit.SECONDS);
assertEquals(result, "123");
Streams TestKit
You may have noticed various code patterns that emerge when testing stream
pipelines. Akka Stream has a separate akka-stream-testkit
module that
provides tools specifically for writing stream tests. This module comes with
two main components that are TestSource
and TestSink
which
provide sources and sinks that materialize to probes that allow fluent API.
注釈
Be sure to add the module akka-stream-testkit
to your dependencies.
A sink returned by TestSink.probe
allows manual control over demand and
assertions over elements coming downstream.
final Source<Integer, NotUsed> sourceUnderTest = Source.from(Arrays.asList(1, 2, 3, 4))
.filter(elem -> elem % 2 == 0)
.map(elem -> elem * 2);
sourceUnderTest
.runWith(TestSink.probe(system), mat)
.request(2)
.expectNext(4, 8)
.expectComplete();
A source returned by TestSource.probe
can be used for asserting demand or
controlling when stream is completed or ended with an error.
final Sink<Integer, NotUsed> sinkUnderTest = Sink.cancelled();
TestSource.<Integer>probe(system)
.toMat(sinkUnderTest, Keep.left())
.run(mat)
.expectCancellation();
You can also inject exceptions and test sink behaviour on error conditions.
final Sink<Integer, CompletionStage<Integer>> sinkUnderTest = Sink.head();
final Pair<TestPublisher.Probe<Integer>, CompletionStage<Integer>> probeAndCompletionStage =
TestSource.<Integer>probe(system)
.toMat(sinkUnderTest, Keep.both())
.run(mat);
final TestPublisher.Probe<Integer> probe = probeAndCompletionStage.first();
final CompletionStage<Integer> future = probeAndCompletionStage.second();
probe.sendError(new Exception("boom"));
try {
future.toCompletableFuture().get(3, TimeUnit.SECONDS);
assert false;
} catch (ExecutionException ee) {
final Throwable exception = ee.getCause();
assertEquals(exception.getMessage(), "boom");
}
Test source and sink can be used together in combination when testing flows.
final Flow<Integer, Integer, NotUsed> flowUnderTest = Flow.of(Integer.class)
.mapAsyncUnordered(2, sleep -> akka.pattern.PatternsCS.after(
Duration.create(10, TimeUnit.MILLISECONDS),
system.scheduler(),
system.dispatcher(),
CompletableFuture.completedFuture(sleep)
));
final Pair<TestPublisher.Probe<Integer>, TestSubscriber.Probe<Integer>> pubAndSub =
TestSource.<Integer>probe(system)
.via(flowUnderTest)
.toMat(TestSink.<Integer>probe(system), Keep.both())
.run(mat);
final TestPublisher.Probe<Integer> pub = pubAndSub.first();
final TestSubscriber.Probe<Integer> sub = pubAndSub.second();
sub.request(3);
pub.sendNext(3);
pub.sendNext(2);
pub.sendNext(1);
sub.expectNextUnordered(1, 2, 3);
pub.sendError(new Exception("Power surge in the linear subroutine C-47!"));
final Throwable ex = sub.expectError();
assert(ex.getMessage().contains("C-47"));
Contents