Testing Output Only Actors

Problem

A lot of the Pony code you will need to test involves actors that take input and create output that leaves your system. A good example of this is testing writing to a file. How can you verify that the contents of the file are what you expect? You could write the file as normal and then compare its contents to what you were expecting. In the end, though, that doesn't work for everything. What if you are writing to standard out or over a network? Luckily, there is a general purpose pattern to address this problem.

Solution

Our solution draws on three primary elements:

  • Pony promises
  • Stub objects
  • Pony's causal messaging

The code below can be used to test that you are outputting data correctly to a file stream. In this particular case, "correctly" means that when we call print on MyImportantClass with the argument "Hello World!" that we would get "Hello World!" as output from the stream that MyImportantClass is using.

use "ponytest"
use "promises"

class MyImportantClass
  let _stream: OutStream
  new create(s: OutStream) =>
    _stream = s

  fun print(s: String) =>
    _stream.print(s)

actor Main is TestList
  new create(env: Env) => PonyTest(env, this)
  new make() => None

  fun tag tests(test: PonyTest) =>
    test(_TestImportantPrinting)

class iso _TestImportantPrinting is UnitTest
  fun name(): String => "my important printing test"

  fun apply(h: TestHelper) =>
    h.long_test(1_000_000_000)

    let promise = Promise[String]
    promise.next[String](recover this~_fulfill(h) end)

    let stream = _TestStream(promise)
    let important = MyImportantClass(stream)
    important.print("Hello World!")
    stream.written()

  fun tag _fulfill(h: TestHelper, value: String): String =>
    h.assert_eq[String](value, "Hello World!")
    h.complete(true)
    value

  fun timed_out(h: TestHelper) =>
    h.complete(false)

actor _TestStream is OutStream
  let _output: String ref = String
  let _promise: Promise[String]

  new create(promise: Promise[String]) =>
    _promise = promise

  be print(data: ByteSeq) =>
    _collect(data)

  be write(data: ByteSeq) =>
    _collect(data)

  be printv(data: ByteSeqIter) =>
    for bytes in data.values() do
      _collect(bytes)
    end

  be writev(data: ByteSeqIter) =>
    for bytes in data.values() do
      _collect(bytes)
    end

  fun ref _collect(data: ByteSeq) =>
    _output.append(data)

  be written() =>
    let s: String = _output.clone()
    _promise(s)

That's a nice chunk of code, let's break it down and focus on the important bits. Here is the core of our test, the apply method on our test class:

  fun apply(h: TestHelper) =>
    h.long_test(1_000_000_000)

    let promise = Promise[String]
    promise.next[String](recover this~_fulfill(h) end)

    let stream = _TestStream(promise)
    let important = MyImportantClass(stream)
    important.print("Hello World!")
    stream.written()

Note the use of TestHelper.long_test(1_000_000_000), where we tell the test framework that our test will continue to run until an assertion fails or one of TestHelper.complete(true) or TestHelper.complete(false) is called. We also provide it with a 1-second timeout after which it will fail the test with a timeout error.

Remember, we are attempting to verify that when we print to MyImportantClass, we get the correct output on the stream. In a real world example, our class would probably be doing some sort of formatting and wouldn't just be a pass through of the data. Instead of testing file stream directly, we are testing a stub _TestStream that is standing in for a standard library OutStream interface. In real code, this would probably be the concrete actor FileStream or similar. Our stub implements the OutStream interface and records everything we write to it:

actor _TestStream is OutStream
  let _output: String ref = String
  let _promise: Promise[String]

  new create(promise: Promise[String]) =>
    _promise = promise

  be print(data: ByteSeq) =>
    _collect(data)

  be write(data: ByteSeq) =>
    _collect(data)

  be printv(data: ByteSeqIter) =>
    for bytes in data.values() do
      _collect(bytes)
    end

  be writev(data: ByteSeqIter) =>
    for bytes in data.values() do
      _collect(bytes)
    end

  fun ref _collect(data: ByteSeq) =>
    _output.append(data)

  be written() =>
    let s: String = _output.clone()
    _promise(s)

The most interesting method in _TestStream is be written(), what's going on in there?

  be written() =>
    let s: String = _output.clone()
    _promise(s)

When invoked, it takes the promise that was supplied upon construction:

    let stream = _TestStream(promise)

And fulfills it with any data we have collected so far:

    let s: String = _output.clone()
    _promise(s)

Our promise was set up to call the _fulfill method on our test class when the promise is fulfilled.

  fun tag _fulfill(h: TestHelper, value: String): String =>
    h.assert_eq[String](value, "Hello World!")
    h.complete(true)
    value

What's going on in there? Well, we take our string of output that our stub got and compare it to our expected value (in this case "Hello World!") and then indicate that our test is complete. We have access to the TestHelper we need to run assert_eq in _fulfill because when we constructed our promise initially, we created the promise as partially applied, supplying the TestHelper parameter:

    promise.next[String](recover this~_fulfill(h) end)

It's important to note that the above solution only works because we can rely on Pony's causal messaging. That is, each message sent to an actor from another actor will arrive in order. In our test, we call:

    important.print("Hello World!")
    stream.written()

because important.print calls a method on stream:

  fun print(s: String) =>
    _stream.print(s)

stream.written() is guaranteed to happen after the _stream.print(s). Without that guarantee, this test wouldn't work. Without causal messaging, our promise might fire before it ever saw any data and our test could pass sometimes and fail other times.

Discussion

Well, there you have it. How to test our interactions with an actor whose side-effects are only observable outside out system. As mentioned in the Problem section, this pattern can be applied in many different scenarios so long as it involves testing an actor. And the code above can be used to test any actor that implements OutStream. Before we wrap up, let's cover one additional benefit of using a stub to test a FileStream.

The FileStream constructor takes a File object. That's an external dependency. When testing, avoiding external dependencies is good. In the case of a file, that might seem like a relatively benign dependency right up until the day the file system permissions on your test machine change or a directory you depend on isn't there and your tests start failing. The important part of all this is, we've left our process and can't rely on the external party to help our test (nor do we want to).

If you want to see an example of this pattern in the wild, check out the tests for the logger package in the Pony standard library.

results matching ""

    No results matching ""