A mock Akka scheduler to simplify testing scheduler-dependent code.
Table of Contents
Akka Scheduler is a convenient tool to make things happen in the future -- for example, "run this function in 5 seconds" or "run that function every 100 milliseconds".
Let's say you want to periodically run the function myFunction()
in your code via Akka Scheduler:
def myFunction() = ???
val initialDelay = 0.millis
val interval = 100.millis
scheduler.schedule(initialDelay, interval)(myFunction())
Unfortunately, the current Akka implementation apparently does not provide a simple way to test-drive code that relies
on Akka Scheduler (see e.g. Testing Actor Systems). This
project closes this gap by providing a "mock scheduler" and an accompanying "virtual time" implementation so that your
test suite does not degrade into Thread.sleep()
hell.
Please note that the scope of this project is not to become a full-fledged testing kit for Akka Scheduler!
This project is published to Sonatype.
- Releases are available on Maven Central.
- Snapshots are available in the Sonatype Snapshot repository.
When using a release:
// Scala 2.10
libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.10" % "0.2.0")
// Scala 2.11
libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.11" % "0.2.0")
When using a snapshot:
resolvers ++= Seq("sonatype-snapshots" at "https://oss.sonatype.org/content/repositories/snapshots")
// Scala 2.10
libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.10" % "0.3.0-SNAPSHOT")
// Scala 2.11
libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.11" % "0.3.0-SNAPSHOT")
In this example we schedule a one-time task to run in 5 milliseconds from "now". We create an instance of
VirtualTime
, which contains its own
MockScheduler
instance.
Tip: In practice, you rarely create
MockScheduler
instances yourself and instead interact with the scheduler through its enclosingVirtualTime
instance.
Here, think of time.advance()
as the logical equivalent of Thread.sleep()
.
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
// A time instance has its own mock scheduler associated with it
val time = new VirtualTime
// Schedule a one-time task that increments a counter
val counter = new AtomicInteger(0)
time.scheduler.scheduleOnce(5.millis)(counter.getAndIncrement)
time.advance(4.millis)
assert(time.elapsed == 4.millis)
assert(counter.get == 0) // <<< not yet, still too early
time.advance(1.millis)
assert(time.elapsed == 5.millis)
assert(counter.get == 1) // <<< task was run at the right time!
In your code you may want to make the scheduler configurable. In the following example the class Foo
has a field
scheduler
that defaults to Akka's system.scheduler
(cf. akka.actor.ActorSystem#scheduler
).
class Foo(scheduler: Scheduler = system.scheduler) {
scheduler.scheduleOnce(500.millis)(bar())
def bar: Unit = ???
}
During testing you can then plug in the mock scheduler:
val time = VirtualTime
val foo = Foo(time.scheduler)
// ...actual tests follow...
See MockSchedulerSpec for further details and examples.
You can also run the include test suite, which includes MockSchedulerSpec
, to improve your understanding of how
the mock scheduler and virtual time work:
$ ./sbt clean test
Example output:
[info] FakeCancellableSpec:
[info] FakeCancellable
[info] - should return false when cancelled
[info] + Given an instance
[info] + When I cancel it
[info] + Then then it returns false
[info] - isCancelled should return false when cancel was not called yet
[info] + Given an instance
[info] + When I ask whether it has been successfully cancelled
[info] + Then then it returns false
[info] - isCancelled should return false when cancel was called already
[info] + Given an instance
[info] + And the instance was cancelled
[info] + When I ask whether it has been successfully cancelled
[info] + Then then it returns false
[info] MockSchedulerSpec:
[info] MockScheduler
[info] - should run a one-time task once
[info] + Given a time with a scheduler
[info] + And and an execution context
[info] + When I schedule a one-time task
[info] + Then the task should not run before its delay
[info] + And the task should run at the time of its delay
[info] + And the task should not run again
[info] - should run a recurring task multiple times
[info] + Given a time with a scheduler
[info] + And and an execution context
[info] + When I schedule a recurring task
[info] + Then the task should not run before its initial delay
[info] + And it should run at the time of its initial delay (run #1)
[info] + And it should not run again before its next interval
[info] + And it should run again at its next interval (run #2)
[info] + And it should not run again before its next interval
[info] + And it should run again at its next interval (run #3)
[info] + And it should have run 103 times after the initial delay and 102 intervals
[info] - should run tasks in order
[info] + Given a time with a scheduler
[info] + And and an execution context
[info] + When I schedule a recurring task A
[info] + And I schedule a one-time task B to run when A has already been run a couple of times
[info] + Then A should run before B
[info] + And A should continue to run after B finished
[info] - should run one-time tasks in order of their registration with the scheduler
[info] + Given a time with a scheduler
[info] + And and an execution context
[info] + When I schedule a one-time task A
[info] + And I then schedule a one-time task B to run at the same time as A
[info] + Then A should run before B
[info] - should, for tasks that are scheduled for the same time, run one-time tasks before subsequent runs of recurring tasks
[info] + Given a time with a scheduler
[info] + And and an execution context
[info] + When I schedule a recurring task A
[info] + And I schedule two one-time tasks B and C, which are scheduled at the same time as A's recurring runs
[info] + Then B and C should happen before the recurring runs of A
[info] - should support recursive scheduling
[info] + Given a time with a scheduler
[info] + And and an execution context
[info] + When I schedule a task A that schedules another task B
[info] + And I advance the time so that A was already run (and thus B is now registered with the scheduler)
[info] + Then B should be run with the configured delay (which will happen in one of the next ticks of the scheduler)
[info] VirtualTimeSpec:
[info] VirtualTime
[info] - should start at time zero
[info] + Given no time
[info] + When I create a time
[info] + Then its elapsed time should be zero
[info] - should track elapsed time
[info] + Given a time
[info] + When I advance the time
[info] + Then the elapsed time should be correct
[info] - should accept a step defined as a Long that represents the number of milliseconds
[info] + Given a time
[info] + When I advance the time by a Long value of 1234
[info] + Then the elapsed time should be 1234 milliseconds
[info] - should have a meaningful string representation
[info] + Given a time
[info] + When I request its string representation
[info] + Then the representation should include the elapsed time in milliseconds
[info] Run completed in 341 milliseconds.
[info] Total number of tests run: 13
[info] Suites: completed 3, aborted 0
[info] Tests: succeeded 13, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
- If you call
time.advance()
, then the scheduler will run any tasks that need to be executed in "one big swing": there will be no delay in-between tasks runs, however the execution order of the tasks is honored.- Example:
time.elapsed
is0 millis
. TasksA
andB
are scheduled to run with a delay of10 millis
and20 millis
, respectively. If you nowadvance()
the time straight to50 millis
, then A will be executed first and, once A has finished and without any further delay, B will be executed immediately.
- Example:
- Tasks are executed synchronously when the scheduler's
tick()
method is called. - For simplicity reasons the
akka.actor.Cancellable instances returned by
this scheduler are not really functional. The
Cancellable.cancel()
method is a no-op and will always return false. This has the effect thatCancellable.isCancelled
will always return false, too, to adhere to theCancellable
contract.
# Runs the tests for the main Scala version only (currently: 2.10.x)
$ ./sbt test
# Runs the tests for all configured Scala versions
$ ./sbt "+ test"
-
Make sure that the version identifier in
version.sbt
has a-SNAPSHOT
suffix. -
Publish the snapshot:
$ ./sbt "+ test" && ./sbt "+ publishSigned"
-
Make sure that the version identifier in
version.sbt
DOES NOT have a-SNAPSHOT
suffix. -
Publish the release artifacts into the Sonatype staging repository:
$ ./sbt "+ test" && ./sbt "+ publishSigned"
-
Follow the Sonatype instructions to release a deployment.
See CHANGELOG.
Copyright © 2014-2015 Michael G. Noll
See LICENSE for licensing information.
The code in this project was inspired by MockScheduler and MockTime in the Apache Kafka project.
See also NOTICE.