gopactor

package module
v0.0.0-...-8be34b0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 19, 2019 License: MIT Imports: 4 Imported by: 0

README

Gopactor - testing for ProtoActor

Gopactor is a set of tools to simplify writing BDD tests for actors created with Protoactor.

GoDoc Go Report Card Build Status

Currently, the main focus is to provide convenient assertions for tests written using the Goconvey framework. However, all provided assertions can potentially be used independently, and it is easy to write an adapter to whatever matcher/assertion library you prefer.

Any contribution to this project will be highly appreciated!

Example of usage

Here is a short example. We'll define and test a simple worker actor that can do only one thing: respond "pong" when it receives "ping".

package worker_test

import (
    "testing"

    "github.com/AsynkronIT/protoactor-go/actor"
    . "github.com/meAmidos/gopactor"
    . "github.com/smartystreets/goconvey/convey"
)

// Actor to test
type Worker struct{}

// This actor can do only one thing, but it does this thing well.
func (w *Worker) Receive(ctx actor.Context) {
    switch m := ctx.Message().(type) {
    case string:
        if m == "ping" {
            ctx.Respond("pong")
        }
    }
}

// For the sake of simplicity, the test is flat and not structured.
// In real life, you may want to follow the "Given-When-Then" approach
// which is commonly recognized as a good BDD-style.
func TestWorker(t *testing.T) {
    Convey("Test the worker actor", t, func() {

        // It is essential to spawn the tested actor using Gopactor. This way, Gopactor
        // will be able to intercept all inbound/outbound messages of the actor.
        worker, err := SpawnFromInstance(&Worker{}, OptDefault.WithPrefix("worker"))
        So(err, ShouldBeNil)

        // Spawn an additional actor that will communicate with our worker.
        // The only purpose of this actor is to be a sparring partner,
        // so we don't care about its functionality.
        // Conveniently, Gopactor provides an easy way to create it.
        requestor, err := SpawnNullActor()
        So(err, ShouldBeNil)

        // Let the requestor ping the worker
        worker.Request("ping", requestor)

        // Assert that the worker receives the ping message
        So(worker, ShouldReceive, "ping")

        // Assert that the worker sends back the correct response
        So(worker, ShouldSendTo, requestor, "pong")

        // Finally, assert that the requestor gets the response
        So(requestor, ShouldReceive, "pong")
    })
}

Main features

Intercept messages

For any actor you want to test, Gopactor can intercept all it's inbound and outbound messages. It is probably exactly what you want to do when you test the actor's behavior. Moreover, interception forces a naturally asynchronous actor to act in a more synchronous way. When messages are sent and received under the control of Gopactor, it is much easier to reason about the actor's logic and examine its communication with the outside world step by step.

Intercept system messages

Protoactor uses some specific system messages to control the lifecycle of an actor. Gopactor can intercept some of such messages to help you test that your actor stops or restarts when expected.

Intercept spawning of children

It is a common pattern to let actors spawn child actors and communicate with them. Good as it is, this pattern often stays in the way of writing deterministic tests. Given that child-spawning and communication happen in the background asynchronously, it can be seen more like a side-effect that can interfere with our tests in many unpredictable ways.

By default, Gopactor intercepts all spawn invocations and instead of spawning what is requested, it spawns no-op null-actors. These actors are guaranteed to not communicate with their parents in any way. If you do no want Gopactor to substitute spawned actors, you can easily disable this behavior via configuration options.

Goconvey-style assertions

Gopactor provides a bunch of assertion functions to be used with the very popular testing framework Goconvey (http://goconvey.co/). For instance,

So(worker, ShouldReceive, "ping")
So(worker, ShouldSendTo, requestor, "pong")
Configurable

For every tested actor, you can define what you want to intercept: inbound, outbound or system messages. Or everything. Or nothing at all. You can also set a custom timeout:

options := OptNoInterception.
    WithOutboundInterception().
    WithPrefix("my-actor").
    WithTimeout(10 * time.Millisecond)

Supported assertions

ShouldReceive
ShouldReceiveFrom
ShouldReceiveSomething
ShouldReceiveN

ShouldSend
ShouldSendTo
ShouldSendSomething
ShouldSendN

ShouldNotSendOrReceive

ShouldStart
ShouldStop
ShouldBeRestarting
ShouldObserveTermination

ShouldSpawn

Plans

Many things could be done to improve the library. Some of the areas that I am personally interested in (with no particular order):

  • Review the interception of child-spawning
  • Add assertions for spawning
  • Ensure thread safety
  • Catch more system messages
  • Add an optional logger
  • Add negative-scenario assertions (ShouldNotReceive, etc.)
  • Be smart in handling/asserting actors failures
  • Handle outbound system messages separately

Contribution

Please feel free to open an issue if you encounter a problem with the library or have a question. Pull requests will be highly appreciated.

Documentation

Overview

Package Gopactor provides a set of tools to simplify testing of actors created with Protoactor (https://github.com/AsynkronIT/protoactor-go).

Main features:

Intercept messages

For any actor you want to test, Gopactor can intercept all it's inbound and outbound messages. It is probably exactly what you want to do when you test the actor's behavior. Moreover, interception forces a naturally asynchronous actor to act in a more synchronous way. When messages are sent and received under the control of Gopactor, it is much easier to reason about the actor's logic and examine its communication with the outside world step by step.

Intercept system messages

Protoactor uses special system messages to control the lifecycle of an actor. Gopactor can intercept some of such messages to help you ensure that your actor stops or restarts when expected.

Intercept spawning of children

It is a common pattern to let actors spawn child actors and communicate with them. Good as it is, this pattern often stays in the way of writing deterministic tests. Given that child-spawning and communication happen in the background asynchronously, it can be seen more like a side-effect that can interfere with your tests in many unpredictable ways.

By default, Gopactor intercepts all spawn invocations and instead of spawning what is requested, it spawns no-op null-actors. These actors are guaranteed to not communicate with their parents in any way. If you do no want Gopactor to substitute spawned actors, you can easily disable this behavior via configuration options.

Goconvey-style assertions

Gopactor provides a bunch of assertion functions to be used with the popular testing framework Goconvey (http://goconvey.co/). For instance,

So(worker, ShouldReceive, "ping")
So(worker, ShouldSendTo, requestor, "pong")

Configurable

For every tested actor, you can define what you want to intercept: inbound, outbound or system messages. Or spawning of children. Or everything. Or nothing at all. You can also set a custom timeout:

options := OptNoInterception.
	WithOutboundInterception().
	WithPrefix("my-actor").
	WithTimeout(10 * time.Millisecond)

Example of usage

Here is a short example. We'll define and test a simple worker actor that can do only one thing: respond "pong" when it receives "ping".

package worker_test

import (
	"testing"

	"github.com/AsynkronIT/protoactor-go/actor"
	. "github.com/meAmidos/gopactor"
	. "github.com/smartystreets/goconvey/convey"
)

// Actor to test
type Worker struct{}

// This actor is very simple. It can do only one thing, but it does this thing well.
func (w *Worker) Receive(ctx actor.Context) {
	switch m := ctx.Message().(type) {
	case string:
		if m == "ping" {
			ctx.Respond("pong")
		}
	}
}

func TestWorker(t *testing.T) {
	Convey("Test the worker actor", t, func() {

		// It is essential to spawn the tested actor using Gopactor. This way, Gopactor
		// will be able to intercept all inbound/outbound messages of the actor.
		worker, err := SpawnFromInstance(&Worker{}, OptDefault.WithPrefix("worker"))
		So(err, ShouldBeNil)

		// Spawn an additional actor that will communicate with our worker.
		// The only purpose of this actor is to be a sparring partner,
		// so we don't care about its functionality.
		// Conveniently, Gopactor provides an easy way to create it.
		requestor, err := SpawnNullActor()
		So(err, ShouldBeNil)

		// Let the requestor ping the worker
		worker.Request("ping", requestor)

		// Assert that the worker receives the ping message
		So(worker, ShouldReceive, "ping")

		// Assert that the worker sends back the correct response
		So(worker, ShouldSendTo, requestor, "pong")

		// Finally, assert that the requestor gets the response
		So(requestor, ShouldReceive, "pong")
	})
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	OptNoInterception           = options.OptNoInterception
	OptDefault                  = options.OptDefault
	OptOutboundInterceptionOnly = options.OptOutboundInterceptionOnly
	OptInboundInterceptionOnly  = options.OptInboundInterceptionOnly
)

Configuration options are explained in detail in the documentation for the options package: https://godoc.org/github.com/meAmidos/gopactor/options

View Source
var (
	ShouldReceive          = assertions.ShouldReceive
	ShouldReceiveType      = assertions.ShouldReceiveType
	ShouldReceiveFrom      = assertions.ShouldReceiveFrom
	ShouldReceiveSomething = assertions.ShouldReceiveSomething
	ShouldReceiveN         = assertions.ShouldReceiveN

	ShouldSend          = assertions.ShouldSend
	ShouldSendType      = assertions.ShouldSendType
	ShouldSendTo        = assertions.ShouldSendTo
	ShouldSendSomething = assertions.ShouldSendSomething
	ShouldSendN         = assertions.ShouldSendN

	ShouldNotSendOrReceive = assertions.ShouldNotSendOrReceive

	ShouldStart              = assertions.ShouldStart
	ShouldStop               = assertions.ShouldStop
	ShouldBeRestarting       = assertions.ShouldBeRestarting
	ShouldObserveTermination = assertions.ShouldObserveTermination

	ShouldSpawn = assertions.ShouldSpawn
)

These assertions are mostly self-explanatory, but it may be helpful to go through some examples which can be found in the documentation for the assertions package: https://godoc.org/github.com/meAmidos/gopactor/assertions

Functions

func PactReset

func PactReset()

PactReset cleans up internal data structures used by Gopactor. Normally, you do not have to use it. If you just test a dozen of actors in a short-living test, there is no need to care about cleaning up. However, if for some reason, you are spawning thousands of actors in a long-running test, you might want to call this function from time to time.

func SpawnFromFunc

func SpawnFromFunc(f actor.ActorFunc, opts ...options.Options) *actor.PID

Analog of Protoactor's actor.SpawnPrefix(actor.FromFunc(...))

Example
ctx := actor.EmptyRootContext

f := func(ctx actor.Context) {
	if msg, ok := ctx.Message().(string); ok {
		fmt.Printf("Got a message: %s\n", msg)
	}
}

worker := SpawnFromFunc(f)

ctx.Send(worker, "Hello, world!")
ShouldReceiveSomething(worker)
Output:

Got a message: Hello, world!

func SpawnFromProducer

func SpawnFromProducer(producer actor.Producer, opts ...options.Options) *actor.PID

Analog of Protoactor's actor.SpawnPrefix(actor.FromProducer(...))

Example
ctx := actor.EmptyRootContext

producer := func() actor.Actor {
	return &Worker{}
}

worker := SpawnFromProducer(producer)
ctx.Send(worker, "Hello, world!")
Output:

func SpawnNullActor

func SpawnNullActor(opts ...options.Options) *actor.PID

Spawn an actor that does nothing. It can be very useful in tests when all you need is an actor that can play a role of a message sender and a black-hole receiver.

Example
ctx := actor.EmptyRootContext

worker := SpawnFromProducer(func() actor.Actor { return &Worker{} })
requestor := SpawnNullActor()

ctx.RequestWithCustomSender(worker, "ping", requestor)
Output:

Types

This section is empty.

Directories

Path Synopsis
Goconvey-style assertions to be used with actors spawned using Gopactor.
Goconvey-style assertions to be used with actors spawned using Gopactor.
Package options defines configuration options that are used by Gopactor when spawning actors.
Package options defines configuration options that are used by Gopactor when spawning actors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL