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-modulemoduleVersion = 1.0extensionClasses = 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 . |