使用 JUnit 对 akka actors 进行单元测试

Unit testing akka actors with JUnit

最近我尝试为 akka actors 编写一些单元测试来测试 actors 消息流。我在测试中观察到一些奇怪的行为:

字段:

private TestActorRef<Actor> sut;
private ActorSystem system;


JavaTestKit AnotherActor;
JavaTestKit YetAnotherActor;

系统和actor在@Before注解方法中创建:

@Before
public void setup() throws ClassNotFoundException {
    system = ActorSystem.apply();

    AnotherActor = new JavaTestKit(system);
    YetAnotherActor  = new JavaTestKit(system);


    Props props = MyActor.props(someReference);

    this.sut = system.of(props, "MyActor"); }                 

下一个

@Test
public void shouldDoSth() throws Exception {
    // given actor
    MyActor actor = (MyActor) sut.underlyingActor();

    // when 
    SomeMessage message = new SomeMessage(Collections.emptyList());
    sut.tell(message, AnotherActor.getRef());

    // then
    YetAnotherActor.expectMsgClass(
        FiniteDuration.apply(1, TimeUnit.SECONDS),
        YetSomeMessage.class);

}

在我的代码中我有:

private void processMessage(SomeMessage message) {
    final List<Entity> entities = message.getEntities();
    if(entities.isEmpty()) {
        YetAnotherActor.tell(new YetSomeMessage(), getSelf());
        // return;
    }

    if (entities > workers.size()) {
        throw new IllegalStateException("too many tasks to be started !");
    }

}

基本上,有时(很少)这样的测试会失败(在另一个 OS 上),并且会抛出 processMessage 方法的异常(由于业务逻辑导致的 IllegalStateException)。

大部分测试通过,因为 YetAnotherActor 收到了 YetSomeMessage 消息,尽管 IllegateStateException 错误也被抛出并记录在堆栈跟踪中。

正如我从 akka TestActorRef 文档中假设的那样:

This special ActorRef is exclusively for use during unit testing in a single-threaded environment. Therefore, it overrides the dispatcher to CallingThreadDispatcher and sets the receiveTimeout to None. Otherwise, it acts just like a normal ActorRef. You may retrieve a reference to the underlying actor to test internal logic.

我的系统只使用单线程来处理 actor 收到的消息。有人可以解释我为什么尽管断言正确,但测试失败了吗?

当然可以在使用适当的代码发送 YetSomeMessage 后返回,但我不明白另一个线程处理如何导致测试失败。

由于您使用的是TestActorRef,所以您基本上是在进行同步测试。作为一般经验法则,除非确实需要,否则不要使用 TestActorRef。那东西使用 CallingThreadDispatcher,即它会窃取调用者线程来执行 actor。因此,您的谜团的解决方案是演员在与您的测试相同的线程上运行,因此异常最终出现在测试线程上。

幸运的是,您的这个测试用例根本不需要 TestActorRef。您可以将演员创建为普通演员,一切都应该正常(即演员将在适当的单独线程上)。请尽量使用异步测试支持http://doc.akka.io/docs/akka/2.4.0/scala/testing.html#Asynchronous_Integration_Testing_with_TestKit