Flink Check:Property-Based Testing for Apache Flink

Flink

Flink 的测试工具只有 local cluster fake,而且它需要用户花费大量的精力手动生成所有那些与测试中的每个特定功能相关的流(及其相应的输出)。

Flink文档中描述的测试流程序的策略建议使用经典的单元测试框架来测试单个运算符,并使用 local cluster fake 来测试一个完整的程序,同时也参考了一些具有不稳定接口的内部测试工具来测试检查点和状态处理。

然而,在所有这些情况下,用户都需要手动介绍输入流和预期输出,一般来说,输入流和预期输出可能是巨大的,这使得这些测试技术变得繁琐而容易出错。

此外,文档本身指出,状态处理测试 “由于时间上的依赖性,状态处理测试可能很棘手”,这就强调了面向流的测试技术的必要性。

Flink Check

Flink Check 是 Apache Flink 的一个基于属性的测试库,它扩展了 ScalaCheck 的线性时序逻辑运算符,适用于测试 Flink 数据流转换。

基于属性的测试(PBT)是一种自动的黑盒测试技术,它通过生成随机输入并检查获得的输出是否满足给定的属性来测试功能。

FlinkCheck 提供了一个有边界的时间逻辑,用于生成函数的输入和声明属性。这个逻辑是为流媒体系统设计的,它允许用户定义流如何随时间变化,以及哪些属性应该验证相应的输出。

FlinkCheck随机生成指定数量的有限输入流前缀,并对Flink运行时产生的输出流进行评估。

Flink Check 是基于 sscheck,这是 Apache Spark 的一个基于属性的测试库,所以它依赖于 sscheck-core 项目,其中包含了 sscheck 和 Flink Check 共同的代码,特别是系统所基于的 LTLss 逻辑的实现。
LTLss 是一种有限字的离散时间线性时序逻辑,在 Spark Streaming 的 Property-based testing for Spark Streaming 的论文中有详细介绍。

Getting started

Flink Check 已经用 Scala 2.11.8Apache Flink 1.8.0 进行了测试。

Flink Check 可作为 Maven 依赖使用

<dependency>
  <groupId>es.ucm.fdi</groupId>
  <artifactId>flink-check_2.11</artifactId>
  <version>0.0.2</version>
  <type>pom</type>
</dependency>
LTLss & FlinkCheck
LTLssgenerators and their FlinckCheck counterparts
LTLss generatorFlinkCheck generator含义
𝑋 gWindowGen.next(g)生成一个空窗口和一个使用g生成的窗口
☐n gWindowGen.always(g,n)生成连续不断的n个窗口
♢𝗇 gWindowGen.eventually(g,n)生成1到n-1个空窗口,使用g生成最后一个窗口
g1 𝑈𝗇 g2WindowGen.until(g1,g2,n)使用g1生成少于n个窗口并使用g2一次
LTLss formulas and their FlinkCheck counterparts.
LTLss formulaFlinkCheck formula含义
Boolean expr.bSolved(b)基本的公式是布尔函数和常量
𝑋 fFormula.next(f)f对下一个窗口保持成立
☐n fFormula.always(f) during nf对前n个窗口保持成立
♢𝗇 fFormula.eventually(f) during nf对前n个窗口中的任意一个保持成立
f1 𝑈𝗇 f2f1.until(f2) during nf1对前k个窗口(0≤k<n)和f2对k+1窗口保持成立
λᵗ₍ℹ,𝑜₎ fFormula.consume(fr)ℹ当前输入窗口,𝑜当前输出窗口,ᵗ当前时间戳
How properties are executed

属性是通过扩展特征 DataStreamTLProperty 来定义的,该特征包括用于定义属性的 forAllDataStream 方法,该属性为每个测试用例生成 DataStream[in] ,应用 testSubject 以生成 DataStream[out],并检查这些数据流是否满足指定的 formula

type TSeq[A] = Seq[TimedElement[A]]
type TSGen[A] = Gen[TSeq[A]]
type Letter[In, Out] = (DataSet[TimedElement[In]], DataSet[TimedElement[Out]])
def forAllDataStream[In : TypeInformation, 
                    Out : TypeInformation](generator: TSGen[In])
                                          (testSubject: (DataStream[In]) => DataStream[Out])
                                          (formula: FlinkFormula[Letter[In, Out]])
                                          (implicit pp1: TSGen[In] => Pretty): Prop 

每个测试用例分两个阶段执行

  1. 运行测试用例:
    • 在内存中为 case class TimedElement[T](TIMESTAMP:LONG,VALUE:T) 生成测试用例为 Seq[TimedElement[Input]]
    • 将其转换为 DataStream[In] 并应用测试对象以获取 DataStream[Out]
    • 将输入和输出的数据流存储到 Flink 配置的默认文件系统中
    • 由于生成的测试用例是有限序列,因此结果流也是有限的
  2. 评估测试用例:
    • 为了检查 formula,我们通过从文件系统读取输入流和输出流来评估测试用例
    • 每个流都被分割成窗口,作为 case class TimedWindow[T](timestamp: Long, data: DataSet[TimedElement[T]]) 中的一个 Iterator[TimedWindow[T]]
    • 我们对输入流和输出流的窗口迭代器进行压缩,获取一个向 formula 提供消息来检查测试用例是否通过的 TimedWindow 对序列
How time is handled in properties

由于 LTLss 逻辑使用离散时间,而 Flink 流是连续的时间,所以我们必须在两个方向上执行离散和连续的转换。

  • 离散生成器连续化
    • sscheck-core 中的生成器对象 WindowGenPStreamGen 生成值序列,其中每个嵌套序列都是一个窗口。
    • FlinkGenerators.tumblingTimeWindows方法通过将每个嵌套序列理解为为一个指定大小的滚动窗口,将其转换为Gen[Seq[TimedElement[A]]]
    • 在每个窗口内,每个元素的时间戳通过均匀的分布获得一个从窗口开始(包括)到窗口结束(不包括)之间的随机值
      • 可指定开始时间,但通常以0作为标准,因为它使得测试执行日志变得更容易解释
      • 除了将来自 sscheck-core 的生成器与 tumblingTimeWindows 相结合外,还可以使用 Seq[TimedElement[in]] 的任何生成器。唯一的要求是,元素是按时间戳的递增顺序生成的。
  • 为了评估测试用例,连续的输入和输出的 Flink 流被离散化,所以我们可以将其理解为 LTLss logic 中的 letters。为此必须为 formula 指定 StreamDiscretizer,但这只是把数据流分割成窗口的一个标准。目前我们支持tumbling time windowssliding time windows

因此,离散和连续世界之间转换是基于时间的窗口标准来执行的。 在一个属性中,我们可以对 generatorformula 使用不同的窗口化条件,尽管两者应该使用相同的开始时间–在所有标准的默认情况都以0为开始时间。

How we use event time

Flink Check 依赖于 event time 来完成所有工作。使用 DataStream.assignAscendingTimestampsTimedElement中的时间戳指定为生成的输入流中的 event time,因为我们按该顺序生成元素,因此可以很好地工作。我们这样做是为了使测试更具可预测性,因为事件计时不那么依赖于运行测试的硬件的性能。测试对象不需要知道事件时间,并且通过特征 DataStreamTLProperty 的默认配置为每个测试用例创建 StreamExecutionEnvironment(可以通过复写方法 buildFreshStreamExecutionEnvironment 来更改)已经配置为使用 event time。

对于事件时间相关的场景,我们有两个选择。

  • 我们可以使用方法 FlinkGenerators.eventTimeToFieldAssigner 来指定方法 fieldAssigner: Long => A => A 根据分配的事件时间设置生成元素的相关字段,使 event time 与生成的时间戳一致。
    • 例如,对于 case class SensorData(timestamp: Long, sensor_id: Int, concentration: Double) 我们使用 ts => _.copy(timestamp = ts) 作为字段分配器。
  • 使用 Seq[TimedElement[in]] 的自定义生成器,其中我们可以完全控制时间戳
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值