Akka. Тестирование в общем и про тестирование кластера в частности
Продолжая заниматься hello world'ом на akka погрузился в вопрос тестирования акторов.
Общие вещи просты, не вижу смысла пересказывать документацию, остановлюсь только на ключевых моментах и выводах.
Асинхронное vs синхронное тестирование
Есть два подхода синхронное и асинхронное тестирование. Первое в реальной жизни почти никогда не нужно, если только не хочется протестировать какие-то уж совсем внутренние кишки актора. В остальных случаях, лучше тестировать честно, отправляя и принимая ответы от акторов.
TestProbe и TestActor
Часть которую важно понимать. Сначала я думал, что при тестировании будет какая-то чёрная магия, которая позволит мне получать сообщения, летающие между разными акторами.
На деле всё проще. Внутри вашего TestCase создаётся TestProbe и TestActor, которые затем используются для запросов к акторам и анализа приходящих результатов. К сожалению, в документации сразу показывается пример с trait ImplicitSender
, который слегка "гримирует" наличие testActor, что вызвало по началу повышенное количество wtf-per-line.
Соответственно набор стандартных assert'ов на самом деле вызывается у стандартного TestProbe. Конечно же таких TestProbe можно даже создать несколько и, например, поместить в них дополнительные специфичные вам assert'ы.
Отсюда же вывод, что для тестирования parent-child взаимодействия придётся вставлять между ними тестовый актор с проксированием сообщений, в документации описаны способы сделать это. В любом случае production код нужно немного к такому подготовить, другой вопрос, что изменения полезны и для других целей.
Cluster testing
С тестированием кластерных конфигурацию всё не так тривиально.
Для начала есть решение multi-jvm тестирования с плагином для sbt, в документации к akka описано как это всё подружить с тестами, чтобы получить Multi Node Testing. Пригодится и для других задач, когда используется просто akka remote.
Печалит, что нужно серьёзно "испортить" конфиг sbt, но, наверное, можно решить выносом таких тестов в отдельный sbt проект. Также ваша IntelliJ IDEA по понятным причинам про такие тесты ничего знать не будет, так как всё магия работает только в связке с sbt. Думаю, в ScalaIDE будет аналогично.
Простого способа дебажить это тоже нет. Логи не очень удобны, так как валяться в параллель со всех JVM. В идеале нужно писать обёртки, которые будут собирать их по каждой ноде отдельно.
"... напоминает мне игру: "Что? Где? Когда?" называется! Непонятно, что где валяется и когда все это кончится!"
Общерекомендуемый подход писать multi-jvm тесты в одном классе, который будет одинаково выполняться на всех нодах. Это обязывает постоянно следить за тем какой код и где исполняется. Например, написанный в лоб assert будет выполнен на всех нодах, часть из которых может быть ещё не присоединена к кластеру.
Постоянно об это спотыкался, но потом написал себе пару удобных утилиток:
import scala.collection.mutable
import org.scalatest.{ BeforeAndAfterAll, Matchers, Suite }
import akka.cluster.Cluster
import akka.cluster.ClusterEvent.{ CurrentClusterState, MemberUp }
import akka.remote.testconductor.RoleName
import akka.remote.testkit.{ MultiNodeSpec, MultiNodeConfig, MultiNodeSpecCallbacks }
import akka.testkit.ImplicitSender
abstract class MultiNodeBaseSpec(config: MultiNodeConfig)
extends MultiNodeSpec(config)
with Suite
with BeforeAndAfterAll
with MultiNodeSpecCallbacks
with ImplicitSender
with Matchers
{
override def beforeAll() = {
super.beforeAll()
multiNodeSpecBeforeAll()
}
override def afterAll() = {
enterBarrier("before-clean-up")
cleanUp()
enterBarrier("clean-up")
multiNodeSpecAfterAll()
super.afterAll()
}
def cluster: Cluster = Cluster(system)
cluster.subscribe(testActor, classOf[MemberUp])
expectMsgClass(classOf[CurrentClusterState])
println(s"myself address: ${node(myself).address}, role: ${myself.name}")
val currentClusterNodes = mutable.Set[RoleName]()
def joinToCluster(nodes: Seq[RoleName], seedNode: RoleName): Unit = {
currentClusterNodes ++= nodes
// on new nodes await events for all cluster member
runOn(nodes: _*) {
cluster join node(seedNode).address
(receiveN(currentClusterNodes.size).collect { case MemberUp(member) => member.address }.toSet
should contain theSameElementsAs currentClusterNodes.map(node(_).address).toSet)
}
// on existing nodes await events for only new cluster members
runOn((currentClusterNodes -- nodes.toSet).toList: _*) {
(receiveN(nodes.size).collect { case MemberUp(member) => member.address }.toSet
should contain theSameElementsAs nodes.map(node(_).address).toSet)
}
enterBarrier("join-"+ nodes.map(_.name).mkString(","))
}
def runOnJoinedNodes(a: => Unit): Unit =
runOn(currentClusterNodes.toList: _*) {
a
}
def cleanUp(): Unit =
cluster.unsubscribe(testActor)
}
В базовом spec'е выше реализовано:
- подписывание на события кластера
- метод
joinToCluster
для правильного присоединения к кластеру нод - метод
runOnJoinedNodes
для выполнения кода на уже работающих нодах кластера, аналогичный по использованию встроенномуrunOn
Тестирование сети
Есть встроенная поддержка тестирования транспорта и сети с возможностью эмуляции проблем между нодами (blackhole). При этом я надеюсь как-нибудь попробовать приспособить docker, его API и iptables для данных целей, благо multi-jvm, кажется умеет сам в тестах упаковывать тестовую ноду в jar, раскладывать через ssh+rsync, а затем запускать.
Примеры
Можно глянуть, что получилось у меня. Много полезных примеров я обнаружил в самих исходниках akka и в проекте akka crdt.
Вывод
Тестировать akka, даже в сложных конфигурациях можно и нужно, но tooling ещё требует доработки.