Статьи

Тестирование приложения АККА со Споком

AKKA — это инструментарий параллелизма, основанный на сообщениях и актерской модели. Хотя он написан на Scala , AKKA можно использовать в любом языковом проекте на основе JVM . Этот пост пытается восполнить пробел в недостающей информации о написании хороших тестов в проектах JVM полиглотов, использующих AKKA . В многоязычных проектах JVM мой очевидный выбор инструмента тестирования — Spock . Этот инструмент, разработанный Groovy и JUnit , делает написание тестов более увлекательным.

Эта статья не предназначена для AKKA или Spock . Предполагается, что аудитория знает основы Groovy и Spock , а также основы параллелизма актерской модели .

Использование фреймворка AKKA TestKit для тестирования актеров

Для наших целей давайте создадим простого субъекта, который получает сообщение, добавляет к нему префикс Hello и отправляет результат обратно исходному отправителю.

HelloActor.java

1
2
3
4
5
6
public class HelloActor extends UntypedActor {
    @Override
    public void onReceive(Object message) throws Exception {
        sender().tell("Hello " + Objects.toString(message.toString()), self());
    }
}

Тестирование актеров AKKA довольно просто даже из не Scala проекта. Благодаря отличной TestKit описанной в TestKit Тестирование Actor Systems . Простой тест можно записать так, как показано ниже.

HelloActorTest.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class HelloActorTest extends Specification {
 
    @AutoCleanup("shutdown") (1)
    def actorSystem = ActorSystem.create()
 
    def probe = new JavaTestKit(actorSystem) (2)
 
    def "actor should say hello"() {
        given:
        def helloActor = actorSystem.actorOf(Props.create(HelloActor))
        when:
        helloActor.tell("world", probe.ref) (3)
        then:
        probe.expectMsgEquals("Hello world") (4)
    }
}

(1) аннотация, указывающая Spock на очистку переменной после завершения теста, вызывая упомянутый метод, то есть shutdown
(2) JavaTestKit является ядром для среды TestKit, предоставляя инструменты для взаимодействия с участниками
(3) отправить строку world как сообщение для актера, передав экземпляр JavaTestKit в качестве отправителя сообщения
(4) утверждение, что probe получил обратно правильное сообщение, то есть с префиксом Hello

Тестирование расширений AKKA

Расширения AKKA — это легкий и мощный способ расширения основных функций AKKA с помощью особенностей проекта. Давайте расширим нашу систему возможностью использовать произвольное приветствие вместо жестко запрограммированного Hello . Для этой цели — мы можем создать расширение GreetExtension с именем GreetExtension с одним открытым методом. Вызов метода вернет случайное приветственное слово из предопределенного списка.

GreetExtension.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class GreetExtension implements Extension {
 
    public static final ExtensionKey<GreetExtension> KEY = new ExtensionKey<GreetExtension>(GreetExtension.class) {}; (1)
 
    private final Random random;
 
    private final ExtendedActorSystem actorSystem;
 
    public GreetExtension(ExtendedActorSystem actorSystem) {
        this.actorSystem = actorSystem;
        this.random = new Random();
    }
 
    public static final List<String> GREET_WORDS = Arrays.asList("Hello", "Nice to meet you", "What's up");
 
    public String greetWord() {
        return GREET_WORDS.get(random.nextInt(GREET_WORDS.size())); (2)
    }
}

(1) уникальный идентификатор, позволяющий получить расширение из экземпляра ActorSystem
(2) случайно выбрать любое доступное приветственное слово

Для иллюстрации использования расширения AKKA давайте создадим модифицированную версию HelloActor именем GreetExtensionActor . Его поведение будет отличаться от оригинального при использовании GreetExtension для генерации ответа. Актер запросит добавочное слово для приветствия , добавит к нему префикс исходного сообщения и затем ответит отправителю сообщения.

GreetExtensionActor.groovy

1
2
3
4
5
6
7
public class GreetExtensionActor extends UntypedActor {
    @Override
    public void onReceive(Object message) throws Exception {
        GreetExtension greetExtension = GreetExtension.KEY.get(context().system()); (1)
        sender().tell(greetExtension.greetWord() + " " + Objects.toString(message), self());
    }
}

(1) получить расширение AKKA по его идентификатору

Использование AKKA TestKit для тестирования актеров, осведомленных о расширении AKKA

HelloActorTest.java мы могли бы изменить тестовый набор GreetExtensionActor для GreetExtensionActor .

GreetExtensionActorTest.groovy

1
2
3
4
5
6
7
8
9
def "actor should greet via AKKA extension"() {
    given:
    def helloActor = actorSystem.actorOf(Props.create(GreetExtensionActor))
    when:
    helloActor.tell("world", probe.ref)
    then:
    def msg = probe.expectMsgClass(String)
    msg.endsWith("world") && GreetExtension.GREET_WORDS.any { msg.startsWith(it) } (1)
}

(1) поскольку префикс генерируется случайным образом — мы не можем проверить точное соответствие, вместо этого мы проверяем, что ответное сообщение имеет префикс с одним из возможных значений

Насмешливое расширение AKKA

Очевидным недостатком тестового примера выше является зависимость от GreetExtension , поведение которой является недетерминированным. GreetExtensionActor не может быть протестирован изолированно и не может быть протестирован с одним определенным набором значений ввода / вывода. Чтобы преодолеть это — наиболее очевидный вариант — использовать насмешку и внедрить насмешку GreetExtension в актерскую систему. Функцию AKKA и заглушек предоставляет сам Spock , но, к несчастью, AKKA не предоставляет API для замены расширения AKKA на экземпляр-заглушку. К счастью, из-за характера Groovy можно получить доступ к закрытым членам ActorSystem . Используя этот трюк, мы могли бы вручную заменить экземпляр расширения AKKA нашей заглушкой и получить возможность написать тестовый набор с определенным вводом / выводом.

GreetExtensionActorTest.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
def "actor should greet via mocked AKKA extension"() {
    given:
    def helloActor = actorSystem.actorOf(Props.create(GreetExtensionActor))
    and:
    GreetExtension.KEY.get(actorSystem)
    actorSystem.extensions[GreetExtension.KEY] = Stub(GreetExtension) { (1)
        greetWord() >> "Bye"
    }
    when:
    helloActor.tell("world", probe.ref)
    then:
    probe.expectMsgClass(String) == "Bye world"
}

(1) магия здесь , доступ к внутренностям актерской системы, настройка ее значения с помощью заглушки расширения

Расширение функциональности системы Actor с помощью модулей расширения Groovy

Глядя на предыдущий тест, можно обнаружить кусок кода, который может быть дублирован при тестировании. Код используется для замены фактического расширения AKKA на макет.

1
2
3
4
GreetExtension.KEY.get(actorSystem)
actorSystem.extensions[GreetExtension.KEY] = Stub(GreetExtension) {
    greetWord() >> "Bye"
}

Было бы здорово, если бы мы могли извлечь это в служебный метод, а затем использовать его там, где это необходимо. Одна из возможностей — использовать черты Groovy и смешать черты в каждом классе спецификации Spock . Другой вариант, который кажется менее многословным, — это возможность улучшить ActorSystem с помощью нового метода, который сделает эту работу. К счастью, у Groovy есть способ сделать это с помощью модулей расширения .

Во время выполнения мы могли бы добавить метод к любому классу, который будет виден только для классов тестов, не затрагивая производственный код. Чтобы включить его, мы должны поместить файл с именем org.codehaus.groovy.runtime.ExtensionModule в org.codehaus.groovy.runtime.ExtensionModule test/resources/META-INF/services .

org.codehaus.groovy.runtime.ExtensionModule

1
2
3
moduleName = akka-spock-module
moduleVersion = 1.0
extensionClasses = ua.eshepelyuk.blog.ActorSystemExtensionModule

Тогда мы готовы реализовать функциональность модуля расширения.

ActorSystemExtensionModule.groovy

1
2
3
4
5
6
class ActorSystemExtensionModule {
    static <T extends Extension> void mockAkkaExtension(ActorSystem actorSystem, ExtensionId<T> extId, T mock) {
        extId.get(actorSystem)
        actorSystem.extensions[extId] = mock
    }
}

Итак, ActorSystem с mockAkkaExtension метода mockAkkaExtension мы могли бы наконец переписать контрольный пример, как mockAkkaExtension ниже.

GreetExtensionActorTest.groovy

01
02
03
04
05
06
07
08
09
10
11
12
def "actor should greet with mocked AKKA extension, using Groovy extension module"() {
    given:
    def helloActor = actorSystem.actorOf(Props.create(GreetExtensionActor))
    and:
    actorSystem.mockAkkaExtension(GreetExtension.KEY, Stub(GreetExtension) { (1)
        greetWord() >> "Bye cruel"
    })
    when:
    helloActor.tell("world", probe.ref)
    then:
    probe.expectMsgClass(String) == "Bye cruel world"
}

(1) вызов метода ActorSystem экземпляра ActorSystem , которого нет в коде Scala , он добавлен нашим ActorSystemExtensionModule

  • Полный код проекта доступен на My GitHub
Ссылка: Тестирование приложения AKKA со Споком от нашего партнера по JCG Евгения Шепелюка в блоге jk .