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 . |