Почему добавление Thread.sleep делает мои модульные тесты Akka TestKit пройденными?

Java 8 и Akka (Java API) 2.12:2.5.16 здесь. У меня есть следующее сообщение:

public class SomeMessage {
    private int anotherNum;

    public SomeMessage(int anotherNum) {
        this.anotherNum = anotherNum;
    }

    public int getAnotherNum() {
        return anotherNum;
    }

    public void setAnotherNum(int anotherNum) {
        this.anotherNum = anotherNum;
    }
}

И следующий актер:

public class TestActor extends AbstractActor {
    private Integer number;

    public TestActor(Integer number) {
        this.number = number;
    }

    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .matchAny(message -> {
                if (message instanceof SomeMessage) {
                    SomeMessage someMessage = (SomeMessage) message;
                    System.out.println("someMessage contains = " + someMessage.getAnotherNum());
                    someMessage.setAnotherNum(number);
                }
            }).build();
    }
}

И следующий модульный тест:

@RunWith(MockitoJUnitRunner.class)
public class TestActorTest {
    static ActorSystem actorSystem;

    @BeforeClass
    public static void setup() {
        actorSystem = ActorSystem.create();
    }

    @AfterClass
    public static void teardown() {
        TestKit.shutdownActorSystem(actorSystem, Duration.create("10 seconds"), true);
        actorSystem = null;
    }

    @Test
    public void should_alter_some_message() {
        // given
        ActorRef testActor = actorSystem.actorOf(Props.create(TestActor.class, 10), "test.actor");
        SomeMessage someMessage = new SomeMessage(5);

        // when
        testActor.tell(someMessage, ActorRef.noSender());

        // then
        assertEquals(10, someMessage.getAnotherNum());
    }
}

Итак, все, что я пытаюсь проверить, это то, что TestActor действительно получает SomeMessage и изменяет свое внутреннее состояние.

Когда я запускаю этот модульный тест, он терпит неудачу и как будто актор никогда не получает сообщение:

java.lang.AssertionError: 
Expected :10
Actual   :5
 <Click to see difference>

    at org.junit.Assert.fail(Assert.java:88)
    at org.junit.Assert.failNotEquals(Assert.java:834)
    at org.junit.Assert.assertEquals(Assert.java:645)
  <rest of trace omitted for brevity>

[INFO] [01/30/2019 12:50:26.780] [default-akka.actor.default-dispatcher-2] [akka://default/user/test.actor] Message [myapp.actors.core.SomeMessage] without sender to Actor[akka://default/user/test.actor#2008219661] was not delivered. [1] dead letters encountered. If this is not an expected behavior, then [Actor[akka://default/user/test.actor#2008219661]] may have terminated unexpectedly, This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.

Но когда я модифицирую тестовый метод и добавляю в него Thread.sleep(5000) (после tell(...)), он проходит с честью:

@Test
public void should_alter_some_message() throws InterruptedException {
    // given
    ActorRef testActor = actorSystem.actorOf(Props.create(TestActor.class, null, 10), "test.actor");
    SomeMessage someMessage = new SomeMessage(5);

    // when
    testActor.tell(someMessage, ActorRef.noSender());

    Thread.sleep(5000);

    // then
    assertEquals(10, someMessage.getAnotherNum());
}

Что здесь происходит?! Очевидно, я не хочу, чтобы мои тесты актеров были замусорены sleeps, так что же я здесь делаю неправильно и как это исправить? Заранее спасибо!


person hotmeatballsoup    schedule 30.01.2019    source источник


Ответы (2)


@Asier Aranbarri прав, говоря, что вы не позволяете актеру закончить свою работу.

Актеры имеют асинхронную природу, и хотя они не реализуют Runnable, они выполняются отдельно от потока, который используется для отправки сообщения.

Вы отправляете сообщение актеру, а затем сразу же утверждаете, что сообщение было изменено. Так как актор работает в асинхронном контексте, т.е. в другом потоке, он еще не обработал входящее сообщение. Таким образом, установка Threed.sleep позволяет актору обработать сообщение и только после этого выполняется утверждение.

Я могу предложить некоторые изменения в вашем первоначальном дизайне, которые будут хорошо сочетаться с природой акка.

Во-первых, akka не предлагает использовать сообщения с изменяемостью. Они должны быть неизменны. В вашем случае это не работает с методом SomeMessage#setAnotherNum. Убери это:

public class SomeMessage {
    private int anotherNum;

    public SomeMessage(int anotherNum) {
        this.anotherNum = anotherNum;
    }

    public int getAnotherNum() {
        return anotherNum;
    }
}

После этого создайте новый экземпляр SomeMessage вместо изменения входящего сообщения в TestActor и отправьте его обратно на context.sender(). Как определено здесь

static public class TestActor extends AbstractActor {
    private Integer number;

    public TestActor(Integer number) {
        this.number = number;
    }

    @Override
    public Receive createReceive() {
        return receiveBuilder()
                .matchAny(message -> {
                    if (message instanceof SomeMessage) {
                        SomeMessage someMessage = (SomeMessage) message;
                        System.out.println("someMessage contains = " + someMessage.getAnotherNum());
                        context().sender().tell(new SomeMessage(number + someMessage.getAnotherNum()), context().self());
                    }
                }).build();
    }
}

Теперь вместо изменения внутреннего состояния сообщения создается новое сообщение с новым состоянием, а более позднее сообщение возвращается обратно в sender(). Это правильное использование akka.

Это позволяет тесту использовать TestProbe и быть переопределенным следующим образом.

@Test
public void should_alter_some_message() {
    // given
    ActorRef testActor = actorSystem.actorOf(Props.create(TestActor.class,10));
    TestJavaActor.SomeMessage someMessage = new SomeMessage(5);
    TestProbe testProbe = TestProbe.apply(actorSystem);

    // when
    testActor.tell(someMessage, testProbe.ref());

    // then
    testProbe.expectMsg(new SomeMessage(15));
}

TestProbe эмулирует отправителя и фиксирует все входящие сообщения/ответы от TestActor. Обратите внимание, что вместо утверждения используется expectMsg(new SomeMessage(15)). Он имеет внутренний механизм блокировки, который ожидает получения сообщения, прежде чем будет выполнено утверждение. Это то, что происходит в примере тестирования субъектов.

Чтобы expectMsg утверждалось правильно, вы должны переопределить метод equals в своем классе SomeMessage.

Редактировать:

Почему Akka не одобряет изменение внутреннего состояния SomeMessage?

Одним из преимуществ akka является то, что он не требует синхронизации или ожидания/уведомления для управления доступом к общим данным. Но этого можно добиться только при неизменности сообщения. Представьте, что вы отправляете изменяемое сообщение, которое вы изменяете точно в то время, когда актор его обрабатывает. Это может вызвать состояние гонки. Прочитайте это для более подробной информации.

И (2) относится ли то же самое к изменению внутреннего состояния Актеров? Допустимо ли, чтобы у ActorRefs были свойства, которые можно изменить, или сообщество также осуждает это (и если да, то почему!)?

Нет, здесь это не применимо. Если какое-либо состояние инкапсулировано в актор и только он может его изменить, изменчивость — это нормально.

person Ivan Stanislavciuc    schedule 30.01.2019
comment
Спасибо @Ivan (+1), все, что вы говорите, имеет смысл! Однако у меня всего два коротких вопроса о характере Акки, о которых вы упомянули, если вы не возражаете! (1) Почему Akka не одобряет изменение внутреннего состояния SomeMessage? Есть ли шанс, что вы могли бы уточнить неприятную ситуацию, созданную изменением внутреннего состояния сообщения? И (2) относится ли то же самое к изменению внутреннего состояния Актеров? Можно ли ActorRefs иметь свойства, которые можно изменить, или сообщество тоже осуждает это (и если да, то почему!)? Еще раз спасибо за замечательный ответ здесь! - person hotmeatballsoup; 31.01.2019
comment
отличное объяснение, мне было интересно. +1 - person aran; 14.02.2019

Я думаю, вы не позволяете актеру делать свою работу. Может быть, AkkaActor запустит свою тему? Я думаю, что Actor действительно реализует Runnable, но на самом деле я не эксперт по Akka. --> редактировать Актер - это интерфейс, рад, что я сказал, что я не эксперт..

Я предполагаю, что, засыпая основной поток, вы даете время "потоку" Актера завершить свой метод.

Я знаю, что это может быть бесполезно, но было слишком долго, чтобы комментировать. : (

person aran    schedule 30.01.2019
comment
Спасибо @Asier (+1) Я ценю помощь, но, как вы видите, я следую тому же базовому шаблону, что и документы Akka TestKit сами прописывают. В этих документах нет Thread.sleeps! Я согласен с вами в принципе, но из того, что я могу сказать, я правильно использую API Akka TestKit, и они, конечно, не ожидают, что их пользователи будут помещать уродливые Thread.sleep в каждый модульный тест! Тут что-то еще не так... - person hotmeatballsoup; 30.01.2019
comment
@hotmeatballsoup, только что нашел это: актер" title="как мне протестировать актера akka, который отправляет сообщение другому актеру"> stackoverflow.com/questions/29270024/, возможно, это может быть полезно - person aran; 30.01.2019
comment
Еще раз спасибо, но в этом другом вопросе используется Scala, а API-интерфейсы Akka Java и Akka Scala значительно отличаются друг от друга. Я также не вижу в них ничего, что бросалось бы мне в глаза как ага! виновник моей проблемы, но я очень ценю вашу помощь здесь! - person hotmeatballsoup; 30.01.2019