项目作者: miguno

项目描述 :
A mock Akka scheduler to simplify testing scheduler-dependent code
高级语言: Scala
项目地址: git://github.com/miguno/akka-mock-scheduler.git
创建时间: 2014-09-23T15:04:06Z
项目社区:https://github.com/miguno/akka-mock-scheduler

开源协议:Other

下载


akka-mock-scheduler

A mock Akka scheduler to simplify testing scheduler-dependent code.

Build Status
Coverage Status
License




Table of Contents


Motivation

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:

  1. def myFunction() = ???
  2. val initialDelay = 0.millis
  3. val interval = 100.millis
  4. 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!

Usage

Build dependency

This project is published to Sonatype.

Latest release (works with Java 8+; you can also
browse the release repository):

  • Scala 2.13:
  • Scala 2.12:
  • Scala 2.11:
  1. // Scala 2.13
  2. libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.13" % "0.5.5")
  3. // Scala 2.12
  4. libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.12" % "0.5.5")
  5. // Scala 2.11
  6. libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.11" % "0.5.5")

Scala 2.10 support is deprecated: the latest release for Scala 2.10 and Java 7 is 0.4.0.

  1. // Scala 2.10
  2. libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.10" % "0.4.0")

Latest snapshot (works with Java 8+; you can also
browse the snapshot repository):

  1. resolvers ++= Seq("sonatype-snapshots" at "https://oss.sonatype.org/content/repositories/snapshots")
  2. // Scala 2.13
  3. libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.13" % "0.5.6-SNAPSHOT")
  4. // Scala 2.12
  5. libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.12" % "0.5.6-SNAPSHOT")
  6. // Scala 2.11
  7. libraryDependencies ++= Seq("com.miguno.akka" % "akka-mock-scheduler_2.11" % "0.5.6-SNAPSHOT")

Examples

First step: start sbt console

You can interactively test-drive the examples below via sbt console, which will automatically make the
akka-mock-scheduler library (plus dependencies such as Akka) available in the console:

  1. $ ./sbt console

Then you can copy-paste the example code below to play around.

Example 1

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 enclosing VirtualTime instance.

Here, think of time.advance() as the logical equivalent of Thread.sleep().

  1. import scala.concurrent.ExecutionContext.Implicits.global
  2. import scala.concurrent.duration._
  3. import com.miguno.akka.testing.VirtualTime
  4. // A time instance has its own mock scheduler associated with it
  5. val time = new VirtualTime
  6. // Schedule a one-time task that increments a counter
  7. val counter = new java.util.concurrent.atomic.AtomicInteger(0)
  8. time.scheduler.scheduleOnce(5.millis)(counter.getAndIncrement)
  9. time.advance(4.millis)
  10. assert(time.elapsed == 4.millis)
  11. assert(counter.get == 0) // <<< not yet incremented, still too early
  12. time.advance(1.millis)
  13. assert(time.elapsed == 5.millis)
  14. assert(counter.get == 1) // <<< now incremented, which means the task was run at the right time!

Example 2

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).

  1. import scala.concurrent.ExecutionContext.Implicits.global
  2. import scala.concurrent.duration._
  3. val system = akka.actor.ActorSystem("my-system")
  4. class Foo(scheduler: akka.actor.Scheduler = system.scheduler) {
  5. scheduler.scheduleOnce(500.millis)(bar())
  6. def bar(): Unit = ???
  7. }

During testing you can then plug in the mock scheduler:

  1. import com.miguno.akka.testing.VirtualTime
  2. val time = new VirtualTime
  3. val foo = new Foo(time.scheduler)
  4. // Actual tests follow, which will leverage `time.advance(...)`.

Further examples

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:

  1. $ ./sbt clean test

Example output:

  1. [info] TaskSpec:
  2. [info] Task
  3. [info] - a task is smaller than another task with a larger delay
  4. [info] + Given an instance
  5. [info] + When compared to a second instance that runs later
  6. [info] + Then the first instance is greater than the second
  7. [info] + And the second instance is smaller than the first
  8. [info] - for two tasks with equal delays, the one with the smaller id is less than the other
  9. [info] + Given an instance
  10. [info] + When compared to second instance with a larger id
  11. [info] + Then the first instance is greater than the second
  12. [info] + And the second instance is smaller than the first
  13. [info] - tasks with equal delays and equal ids are equal
  14. [info] + Given an instance
  15. [info] + When compared to another with the same delay and id
  16. [info] + Then it returns true
  17. [info] VirtualTimeSpec:
  18. [info] VirtualTime
  19. [info] - should start at time zero
  20. [info] + Given no time
  21. [info] + When I create a time
  22. [info] + Then its elapsed time should be zero
  23. [info] - should track elapsed time
  24. [info] + Given a time
  25. [info] + When I advance the time
  26. [info] + Then the elapsed time should be correct
  27. [info] - should accept a step defined as a Long that represents the number of milliseconds
  28. [info] + Given a time
  29. [info] + When I advance the time by a Long value of 1234
  30. [info] + Then the elapsed time should be 1234 milliseconds
  31. [info] - should have a meaningful string representation
  32. [info] + Given a time
  33. [info] + When I request its string representation
  34. [info] + Then the representation should include the elapsed time in milliseconds
  35. [info] - should enforce a minimum advancement of 1 miliseconds
  36. [info] + Given a time
  37. [info] + Then it will throw an exception if time is advanced by less than 1 millisecond
  38. [info] MockSchedulerSpec:
  39. [info] MockScheduler
  40. [info] - should run a one-time task once
  41. [info] + Given a time with a scheduler
  42. [info] + And an execution context
  43. [info] + When I schedule a one-time task
  44. [info] + Then the task should not run before its delay
  45. [info] + And the task should run at the time of its delay
  46. [info] + And the task should not run again
  47. [info] - should run a recurring task multiple times
  48. [info] + Given a time with a scheduler
  49. [info] + And an execution context
  50. [info] + When I schedule a recurring task
  51. [info] + Then the task should not run before its initial delay
  52. [info] + And it should run at the time of its initial delay (run #1)
  53. [info] + And it should not run again before its next interval
  54. [info] + And it should run again at its next interval (run #2)
  55. [info] + And it should not run again before its next interval
  56. [info] + And it should run again at its next interval (run #3)
  57. [info] + And it should have run 103 times after the initial delay and 102 intervals
  58. [info] - should run tasks with different delays in order
  59. [info] + Given a time with a scheduler
  60. [info] + And an execution context
  61. [info] + When I schedule a recurring task A
  62. [info] + And I schedule a one-time task B to run when A has already been run a couple of times
  63. [info] + Then A should run before B
  64. [info] + And A should continue to run after B finished
  65. [info] - should run tasks that are scheduled for the same time in order of their registration with the scheduler
  66. [info] + Given a time with a scheduler
  67. [info] + And an execution context
  68. [info] + When I schedule a one-time task A
  69. [info] + And I then schedule a recurring task B whose initial run is scheduled at the same time as A
  70. [info] + And I then schedule a one-time task C to run at the same time as A
  71. [info] + Then A should run before B, and B should run before C
  72. [info] - should support recursive scheduling
  73. [info] + Given a time with a scheduler
  74. [info] + And an execution context
  75. [info] + When I schedule a task A that schedules another task B
  76. [info] + And I advance the time so that A was already run (and thus B is now registered with the scheduler)
  77. [info] + Then B should be run with the configured delay (which will happen in one of the next ticks of the scheduler)
  78. [info] - should not run a cancelled task
  79. [info] + Given a time with a scheduler
  80. [info] + And an execution context
  81. [info] + When I schedule a one-time task
  82. [info] + And I cancel the task before its execution time
  83. [info] + Then the task should not run
  84. [info] - should not run a recurring task after it was cancelled
  85. [info] + Given a time with a scheduler
  86. [info] + And an execution context
  87. [info] + When I schedule a recurring task
  88. [info] + And I advance the time so that the task is executed once
  89. [info] + And I cancel the task and advance the time further
  90. [info] + Then the task should not run any more
  91. [info] MockCancellableSpec:
  92. [info] MockCancellable
  93. [info] - should return true when cancelled the first time
  94. [info] + Given an instance
  95. [info] + When I cancel it the first time
  96. [info] + Then it returns true
  97. [info] - should return false when cancelled the second time
  98. [info] + Given an instance
  99. [info] + When I cancel it the second time
  100. [info] + Then it returns false
  101. [info] - isCancelled should return false when cancel was not called yet
  102. [info] + Given an instance
  103. [info] + When I ask whether it has been cancelled
  104. [info] + Then it returns false
  105. [info] - isCancelled should return true when cancel was called already
  106. [info] + Given an instance
  107. [info] + And the instance was cancelled
  108. [info] + When I ask whether it has been cancelled
  109. [info] + Then it returns true
  110. [info] Run completed in 360 milliseconds.
  111. [info] Total number of tests run: 19
  112. [info] Suites: completed 4, aborted 0
  113. [info] Tests: succeeded 19, failed 0, canceled 0, ignored 0, pending 0
  114. [info] All tests passed.

Design and limitations

  • 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.
    Note: If tasks are scheduled to run at the same time, then they will be run in the order of their registration with
    the scheduler.
    • Example 1: time.elapsed is 0 millis. Tasks A and B are scheduled to run with a delay of 10 millis and
      20 millis, respectively. If you now advance() the time straight to 50 millis, then A will be executed first
      and, once A has finished and without any further delay, B will be executed immediately.
    • Example 2: time.elapsed is 0 millis. First, task C is scheduled to run with a delay of 300 millis, then
      task D is scheduled to run with a delay of 300 millis, too. If you now advance() the time to 300 millis
      or more, then task C will always be run before task D (because C was registered first).
  • Tasks are executed synchronously when the scheduler’s tick() method is called.

Development

Running the test spec

  1. # Runs the tests for the main Scala version only (currently: 2.13.x)
  2. $ ./sbt test
  3. # Runs the tests for all supported Scala versions
  4. $ ./sbt "+ test"

Publishing to Sonatype

Preparation

Step 1: Ensure that your Sonatype credentials are available to sbt.

For sbt 1.x:

  1. $ cat ~/.sbt/1.0/sonatype.sbt
  2. credentials += Credentials("Sonatype Nexus Repository Manager",
  3. "oss.sonatype.org",
  4. "<sonatype-jira-username>",
  5. "<sonatype-jira-password>")

Step 2: Ensure that your shell environment has set export GPG_TTY=$(tty) (e.g. in ~/.bashrc), otherwise you might
run into the error “gpg: signing failed: Inappropriate ioctl for device”.

Publishing a snaphost

  1. Make sure that the version identifier in version.sbt has a -SNAPSHOT suffix.
  2. Sign and publish the snapshot (note: sonatypeRelease is not needed for snapshots):

    1. $ ./sbt "+ test" && ./sbt "+ publish"

Publishing a release

  1. Make sure that the version identifier in version.sbt DOES NOT have a -SNAPSHOT suffix.
  2. Sign and publish the release artifacts. The commands below will stage, close, and release the artifacts using the
    sbt-sonatype plugin. See also the Sonatype documentation on
    how to release a deployment.

    1. # Publish release for all supported Scala versions
    2. $ ./sbt "+ test" && ./sbt "+ publish" && ./sbt "+ sonatypeRelease"
    3. # Publish release for a specific Scala version only (here: 2.11.12)
    4. $ ./sbt "++ 2.11.12 test" && ./sbt "++ 2.11.12 publish" && ./sbt sonatypeRelease
  3. git tag the release.

Change log

See CHANGELOG.

License

Copyright © 2014-2018 Michael G. Noll

See LICENSE for licensing information.

Credits

The code in this project was inspired by
MockScheduler
and MockTime
in the Apache Kafka project.

See also NOTICE.