MAIN IDEA :我们如何通过相当复杂的业务逻辑对Akka演员进行单元测试(或重新考虑以促进单元测试?)
我一直在使用Akka参与我公司的项目(一些非常基本的东西正在制作中)并且一直在不断重新考虑我的演员,研究和试验Akka测试工具包,看看我能否做到正确...
基本上,我所做的大部分阅读随便说了#34; Man,你只需要测试工具包。如果您正在使用嘲笑,那么您做错了!"然而,文档和示例非常轻松,我发现了许多未涵盖的问题(基本上他们的例子是非常有用的类,有1种方法,与其他演员没有交互,或者只是以琐碎的方式,如输入输出结束时方法)。顺便说一句,如果有人能指出我的akka应用程序的测试套件有任何合理的复杂性,我真的很感激。
在这里,我现在至少会尝试详细说明一些具体情况。我想知道一个人会称之为" Akka认证的"方法(但请不要模糊......我正在寻找Roland Kuhn风格的方法,如果他真的要深入研究具体问题)。我将接受涉及重构的策略,但请注意我在场景中提到的对此的焦虑。
场景1:横向方法(在同一个角色中调用另一个方法)
case class GetProductById(id : Int)
case class GetActiveProductsByIds(ids : List[Int])
class ProductActor(val partsActor : ActorRef, inventoryActor : ActorRef) extends Actor {
override def receive: Receive = {
case GetProductById(pId) => pipe(getProductById(pId)) to sender
case GetActiveProductsByIds(pIds) => pipe(getActiveProductsByIds(pIds)) to sender
}
def getProductById(id : Int) : Future[Product] = {
for {
// Using pseudo-code here
parts <- (partsActor ? GetPartsForProduct(id)).mapTo[List[Part]]
instock <- (inventoryActor ? StockStatusRequest(id)).mapTo[Boolean]
product <- Product(parts, instock)
} yield product
}
def getActiveProductsByIds(ids : List[Int]) : Future[List[Product]] = {
for {
activeProductIds <- (inventoryActor ? FilterActiveProducts(ids)).mapTo[List[Int]]
activeProducts <- Future.sequence(activeProductIds map getProductById)
} yield activeProducts
}
}
所以,基本上我们这里有2个fetch方法,一个是单数,一个是多个。在单一情况下,测试很简单。我们设置了一个TestActorRef,在构造函数中注入了一些探针,并确保正确的消息链正在激活。
我的焦虑来自多重获取方法。它涉及过滤步骤(仅获取活动产品ID)。现在,为了测试这个,我可以设置相同的场景(ProductActor的TestActorRef,其中探测器替换构造函数中调用的actor)。但是,为了测试消息传递流程,我必须模拟所有消息链接,不仅仅是对FilterActiveProducts的响应,而是所有已经被&#34; getProductById&#34;的先前测试所覆盖的消息链接。方法(当时不是真正的单元测试,是吗?)。很明显,这可能会在必要的消息模拟量方面失控,并且更容易验证(通过模拟?)这个方法只是为过滤器中存活的每个ID调用。
现在,我知道这可以通过提取另一个actor来解决(创建一个获取多个ID的ProductCollectorActor,并简单地调用ProductActor,并为每个通过过滤器的ID发出一条消息请求)。但是,我已经计算了这个&amp;如果我要为每一个难以测试的兄弟方法做这样的提取,我会以相对少量的域对象结束几十个演员。样板开销的数量会很多,而且系统会更复杂(更多的演员只是执行基本上是某些方法组合)。
旁白:内联(静态)逻辑
我试图解决这个问题的一种方法是将内联(基本上只是一个非常简单的控制流程)移动到伴侣或另一个单例对象中。例如,如果上述方法中的方法是过滤掉产品,除非它们匹配某种类型,我可能会做类似以下的事情:
object ProductActor {
def passthroughToysOnly(products : List[Product]) : List[Toy] =
products flatMap {p =>
p.category match {
case "toy" => Some(p)
case _ => None
}
}
}
这可以单独进行单元测试,并且实际上可以允许测试非常复杂的单元,只要它们不会呼叫其他演员。我并不是把它们放在使用它们的逻辑上的巨大粉丝(我应该把它放在实际的演员中吗?然后通过调用底层的演员进行测试?)。
总的来说,它仍然会导致这样的问题:在实际调用此方法的方法中进行更天真的基于消息传递的测试时,我基本上必须编辑所有的消息期望,以反映这些数据将如何转换&# 39;静&#39; (我知道他们在Scala中并不是技术上的静态,而是在我身上)。我想我可以忍受,因为它是单元测试的一个现实部分(在一个调用其他几个方法的方法中,我们可能会在存在具有不同属性的测试数据的情况下检查格式塔组合逻辑)。
这一切真的让我失望了 -
场景2:递归算法
case class GetTypeSpecificProductById(id : Int)
class TypeSpecificProductActor(productActor : ActorRef, bundleActor : ActorRef) extends Actor {
override def receive: Receive = {
case GetTypeSpecificProductById(pId) => pipe(getTypeSpecificProductById(pId)) to sender
}
def getTypeSpecificProductById(id : Int) : Future[Product] = {
(productActor ? GetProductById(id)).mapTo[Product] flatMap (p => p.category match {
case "toy" => Toy(p.id, p.name, p.color)
case "bundle" =>
Bundle(p.id, p.name,
getProductsInBundle((bundleActor ? GetProductIdsForBundle(p.id).mapTo[List[Int]))
}
)
}
def getProductsInBundle(ids : List[Int]) : List[Product] =
ids map getProductById
}
所以是的,这里有一些伪代码,但要点是现在我们有一个递归方法(getProductId在bundle的情况下调用getProductsById,它再次调用getProductId)。通过模拟,我们可以切断递归以使事情变得更可测试。但即使这很复杂,因为在方法中某些模式匹配中存在actor调用。
对我来说,这真是一场完美的风暴......为&#34;捆绑&#34;提取匹配。 case进入一个较低的actor可能是有希望的,但那时也意味着我们现在需要处理循环依赖(bundleAssembly actor需要typeSpecificActor,它需要bundleAssembly ......)。
这可以通过纯消息模拟来测试(创建存根消息,我可以测量它们将具有什么级别的递归以及仔细设计此消息序列)但是它将非常复杂&amp;更糟糕的是,如果需要更多的逻辑而不是一个额外的actor调用bundle类型。
说明
提前感谢您的帮助!我实际上对最小,可测试,设计良好的代码充满热情,我担心如果我尝试通过提取实现所有内容,我仍然会遇到循环问题,仍然无法真正测试任何内联/组合逻辑和放大器。我的代码将比用于极小的单一到极端责任的演员的大量样板更加冗长1000倍。从本质上讲,代码都将围绕测试结构编写。
我对过度设计的测试也非常谨慎,因为如果他们正在测试错综复杂的消息序列而不是方法调用(我不确定如何期待简单的语义调用,除了模拟),测试可能会成功但是赢得真正的核心方法功能的真正单元测试。相反,它只是代码(或消息传递系统)中控制流构造的直接反映。
所以也许是因为我要求进行过多次单元测试,但如果你有一些智慧,那就请我直截了当!
答案 0 :(得分:1)
我不同意你的陈述&#34;我并不是把它们放在使用它们的逻辑上的忠实粉丝&#34;。
我发现这是单元测试和代码组织的重要组成部分。
Effective Akka 中的杰米·艾伦(Jamie Allen)阐述了关于外部化业务逻辑(强调我的)的以下内容:这有一些额外的好处。首先,我们不仅可以写作 有用的单元测试,但我们也可以获得有意义的堆栈跟踪 说出函数中发生故障的名称。它也是 阻止我们关闭外部状态,因为一切都必须 作为操作数传递给它。另外,我们可以构建可重用的库 减少代码重复的函数。
编写代码时,我比你的例子更进一步,将业务逻辑移到一个单独的包中:
package businessLogic
object ProductGetter {
def passthroughToysOnly(products : List[Product]) : List[Toy] =
products flatMap {p =>
p.category match {
case "toy" => Some(p)
case _ => None
}
}
}
这允许将并发方法更改为Futures,Java线程,甚至一些尚未创建的并发库,而无需重构我的业务逻辑。业务逻辑包成为&#34;什么&#34;代码,akka库成为&#34;如何&#34;。
如果您隔离了业务逻辑,那么所有接收方法都会变得简单&#34;路由器&#34;消息到外部函数。因此,如果您使用单元测试来掌握业务逻辑,那么您需要对Actors进行的唯一测试就是确保案例模式正确匹配。
解决您的具体问题:我会从Actor中删除getActiveProductsByIds
。如果Actor的用户想要仅获取活动产品,请将其留给他们以首先过滤ID。你的演员应该只做一件事:GetProductById
。再次引用艾伦:
让演员执行添加任务非常容易 - 我们很简单 将新消息添加到其接收块以允许它执行更多和 不同种类的工作。但是,这样做会限制你的能力 组成演员系统并定义上下文分组。保持你的 演员专注于单一的工作,并允许这样做 你自己灵活地使用它们。
答案 1 :(得分:0)
首先,这是一个非常有趣的问题。 Akka文档总的来说非常好,testing部分有很多有见地的注释,以避免常见的陷阱并建议最佳实践。
前几天我reading about this,发现我之前没有尝试过的建议:使用observer pattern。我们的想法是让你的演员只关心消息(你不需要测试,Akka团队会为你做这件事;)并向订阅者广播事件。这样,您的逻辑就会与Actors完全隔离,从而使测试变得更加容易。
注意: 我还没有在生产系统中尝试这个,但是既然你提到生产系统中只有非常基本的东西,那么这可能是值得的。