我的Spark SQL单元测试实践
最近加入一个Spark项目,作为临时的开发人员协助进行开发工作。该项目中不存在测试的概念,开发人员按需求进行编码工作后,直接向生产系统部署,再由需求的提出者在生产系统检验程序运行结果的正确性。在这种原始的工作方式下,产品经理和开发人员总是在生产系统验证自己的需求、代码。可以想见,各种直接交给用户的错误导致了一系列的事故和不信任。为了处理各类线上问题,大家都疲于奔命。当工作进行到后期,每一个相关人都已经意气消沉,常常对工作避之不及。
为了改善局面,我尝试了重构部分代码,将连篇的SQL分散到不同的方法里,并对单个方法构建单元测试。目的是,在编码完成后,首先在本地执行单元测试,以实现:
- 部署到生产系统的代码中无SQL语法错误。
- 将已出现的bug写入测试用例,避免反复出现相同的bug。
- 提前发现一些错误,减少影响到后续环节的问题。
- 通过自动化减少开发和程序问题处理的总时间花费。
- 通过流程和结果的改善,减少开发人员的思维负担,增加与其他相关人的互信。
本文将介绍我的Spark单元测试实践,供大家参考、批评。
本文中的Spark API是PySpark,测试框架为pytest。
对于希望将本文当作单元测试教程使用的读者,本文会假定读者已经准备好了开发和测试所需要的环境。如果没有也没有关系,文末的参考部分会包含一些配置环境相关的链接。
本文链接:https://www.cnblogs.com/hhelibeb/p/10534862.html
原创内容,转载请注明
概念
定义
单元测试是一种测试方法,它的对象是单个程序单元/组件,目的是验证软件的每个组件都符合设计要求。
单元是软件中最小的可测试部分。它通常包含一些输入和单一的输出。
本文中的单元就是python函数(function)。
单元测试通常是程序开发人员的工作。
原则
为了实现单元测试,函数最好符合一个条件,
- 对于相同的输入,函数总有相同的输出。
这要求函数的输出结果不依赖内外部状态。
它的输出结果的确定不应该依赖输入参数外的任何内容,例如,不可以因为本地测试环境中没有相应的数据库就产生“连接数据库异常”导致无法返回结果。如果是类方法的话,也不可以依据一个可能被改变的类属性来决定输出。
同时,函数内部不能存在“副作用”。它不应该改变除了返回结果以外的任何内容,例如,不可以改变全局可变状态。
满足以上条件的函数,可以被称为“纯函数”。
代码实践
下面是数据和程序部分。
数据
假设我们的服务对象是一家水果运销公司,公司在不同城市设有仓库,现有三张表,其中inventory包含水果的总库存数量信息,inventory_ratio包含水果在不同城市的应有比例,
目标是根据总库存数量和比例算出水果在各地的库存,写入到第三张表inventory_city中。三张表的列如下,
1. inventory. Columns: “item”, “qty”.
2. inventory_ratio. Columns: “item”, “city”, “ratio”.
3. inventory_city. Columns: “item”, “city”, “qty”.
第一版代码
用最直接的方式实现这一功能,代码将是,
from pyspark.sql import SparkSession
if __name__ == "__main__":
spark = SparkSession.builder.appName('TestAPP').enableHiveSupport().getOrCreate()
result = spark.sql('''select t1.item, t2.city,
case when t2.ratio is not null then t1.qty * t2.ratio
else t1.qty
end as qty
from v_inventory as t1
left join v_ratio as t2 on t1.item = t2.item ''')
result.write.csv(path="somepath/inventory_city", mode="overwrite")
这段代码可以实现计算各城市库存的需求,但测试起来会不太容易。特别是如果未来我们还要在这个程序中增加其他逻辑的话,不同的逻辑混杂在一起后,测试和修改都会变得麻烦。
所以,在下一步,我们要将部分代码封装到一个函数中。
有副作用的函数
创建一个名为get_inventory_city的函数,将代码包含在内,
from pyspark.sql import SparkSession
def get_inventory_city():
spark = SparkSession.builder.appName('TestAPP').enableHiveSupport().getOrCreate()
result = spark.sql('''select t1.item, t2.city,
case when t2.ratio is not null then t1.qty * t2.ratio
else t1.qty
end as qty
from v_inventory as t1
left join v_ratio as t2 on t1.item = t2.item ''')
result.write.csv(path="somepath/inventory_city", mode="overwrite") if __name__ == "__main__": get_inventory_city()
显然,这是一个不太易于测试的函数,因为它,
- 没有输入输出参数,不能直接根据给定数据检验运行结果。
- 包含对数据库的读/写,这意味着它要依赖外部数据库。
- 包含对spark session的获取/创建,这和计算库存的逻辑也毫无关系。
我们把这些函数中的多余的东西称为副作用。副作用和函数的核心逻辑纠缠在一起,使单元测试变得困难,也不利于代码的模块化。
我们必须另外管理副作用,只在函数内部保留纯逻辑。
无副作用的函数
按照上文中提到的原则,重新设计函数,可以得到,
from pyspark.sql import SparkSession, DataFrame
def get_inventory_city(spark: SparkSession, inventory: DataFrame, ratio: DataFrame):
inventory.createOrReplaceTempView('v_inventory')
ratio.createOrReplaceTempView('v_ratio')
result = spark.sql('''select t1.item, t2.city,
case when t2.ratio is not null then t1.qty * t2.ratio
else t1.qty
end as qty
from v_inventory as t1
left join v_ratio as t2 on t1.item = t2.item ''')
return result
if __name__ == "__main__":
spark = SparkSession.builder.appName('TestAPP').enableHiveSupport().getOrCreate()
inventory = spark.sql('''select * from inventory''')
ratio = spark.sql('''select * from inventory_ratio''')
result = get_inventory_city(spark, inventory, ratio)
result.write.csv(path="somepath/inventory_city", mode="overwrite")
修改后的函数get_inventory_city有3个输入参数和1个返回参数,函数内部已经不再包含对spark session和数据库表的处理,这意味着对于确定的输入值,它总会输出不变的结果。
这比之前的设计更加理想,因为函数只包含纯逻辑,所以调用者使用它时不会再受到副作用的干扰,这使得函数的可测试性和可组合性得到了提高。
测试代码
创建一个test_data目录,将csv格式的测试数据保存到里面。测试数据的来源可以是手工模拟制作,也可以是生产环境导出。
然后创建测试文件,添加代码,
from inventory import get_inventory_city
from pyspark.sql import SparkSession spark = SparkSession.builder.appName('TestAPP').enableHiveSupport().getOrCreate() def test_get_inventory_city(): #导入测试数据
inventory = spark.read.format("csv").option("header", "true").load("./test_data/inventory.csv")
ratio = spark.read.format("csv").option("header", "true").load("./test_data/inventory_ratio.csv") #执行函数
result = get_inventory_city(spark, inventory, ratio) #验证拆分后的总数量等于拆分前的总数量
result.createOrReplaceTempView('v_result')
inventory.createOrReplaceTempView('v_inventory') qty_before_split = spark.sql('''select sum(qty) as qty from v_inventory''')
qty_after_split = spark.sql('''select sum(qty) as qty from v_result''') assert qty_before_split.take(1)[0]['qty'] == qty_after_split.take(1)[0]['qty']
执行测试,可以看到以下输出内容
============================= test session starts =============================
platform win32 -- Python 3.6.8, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
rootdir: C:\Users\zhaozhe42\PycharmProjects\spark_unit\unit, inifile:collected 1 item
test_get_inventory_city.py .2019-03-21 14:16:24 WARN ObjectStore:568 - Failed to get database global_temp, returning NoSuchObjectException
[100%]
========================= 1 passed in 18.06 seconds ==========================
这样一个单元测试例子就完成了。
相比把程序放到服务器测试,单元测试的运行速度更快,开发者不用再担心测试会对生产作业和用户造成影响,也可以更早发现在编码期间犯下的错误。它也可以成为自动化测试的基础。
待解决的问题
目前我已经可以在项目中构建初步的单元测试,但依然面临着一些问题。
运行时间
上面这个简单的测试示例在我的联想T470笔记本上需要花费18.06秒执行完成,而实际项目中的程序的复杂度要更高,执行时间也更长。执行时间过长一件糟糕的事情,因为单元测试的执行花费越大,就会越被开发者拒斥。面对显示器等待单元测试执行完成的时间是难捱的。虽然相比于把程序丢到生产系统中执行,这种单元测试模式已经可以节约不少时间,但还不够好。
接下来可能会尝试的解决办法:提升电脑配置/改变测试数据的导入方式。
有效范围
在生产实践中构建纯函数是一件不太容易的事情,它对开发者的设计和编码能力有相当的要求。
单元测试虽然能帮助发现一些问题和确定问题代码范围,但它似乎并不能揭示错误的原因。只靠单元测试,不能完全证明代码的正确性。
笔者水平有限,目前写出的代码中仍有很多单元测试力所不能及的地方。可能需要在实践中对它们进行改进,或者引入其它测试手段作为补充。
参考
一些参考内容。
配置
Getting Started with PySpark on Windows
阅读
我的Spark SQL单元测试实践的更多相关文章
- 实验5 Spark SQL编程初级实践
今天做实验[Spark SQL 编程初级实践],虽然网上有答案,但都是用scala语言写的,于是我用java语言重写实现一下. 1 .Spark SQL 基本操作将下列 JSON 格式数据复制到 Li ...
- 【原创 Hadoop&Spark 动手实践 9】Spark SQL 程序设计基础与动手实践(上)
[原创 Hadoop&Spark 动手实践 9]SparkSQL程序设计基础与动手实践(上) 目标: 1. 理解Spark SQL最基础的原理 2. 可以使用Spark SQL完成一些简单的数 ...
- 【原创 Hadoop&Spark 动手实践 10】Spark SQL 程序设计基础与动手实践(下)
[原创 Hadoop&Spark 动手实践 10]Spark SQL 程序设计基础与动手实践(下) 目标: 1. 深入理解Spark SQL 程序设计的原理 2. 通过简单的命令来验证Spar ...
- 实验 5 Spark SQL 编程初级实践
实验 5 Spark SQL 编程初级实践 参考厦门大学林子雨 1. Spark SQL 基本操作 将下列 json 数据复制到你的 ubuntu 系统/usr/local/spark 下,并 ...
- 实验5 Spark SQL 编程初级实践
源文件内容如下(包含 id,name,age),将数据复制保存到 ubuntu 系统/usr/local/spark 下, 命名为 employee.txt,实现从 RDD 转换得到 DataFram ...
- Spark SQL 编程初级实践
一.实验目的 (1) 通过实验掌握 Spark SQL 的基本编程方法: (2) 熟悉 RDD 到 DataFrame 的转化方法: (3) 熟悉利用 Spark ...
- Spark SQL在100TB上的自适应执行实践(转载)
Spark SQL是Apache Spark最广泛使用的一个组件,它提供了非常友好的接口来分布式处理结构化数据,在很多应用领域都有成功的生产实践,但是在超大规模集群和数据集上,Spark SQL仍然遇 ...
- 第五周周二练习:实验 5 Spark SQL 编程初级实践
1.题目: 源码: import java.util.Properties import org.apache.spark.sql.types._ import org.apache.spark.sq ...
- spark实验(五)--Spark SQL 编程初级实践(1)
一.实验目的 (1)通过实验掌握 Spark SQL 的基本编程方法: (2)熟悉 RDD 到 DataFrame 的转化方法: (3)熟悉利用 Spark SQL 管理来自不同数据源的数据. 二.实 ...
随机推荐
- Socket.io发送消息含义
仅作收藏:转自博客园 若相忆; // send to current request socket client socket.emit('message', "this is a test ...
- asp.net core系列 35 EF保存数据(2) -- EF系列结束
一.事务 (1) 事务接着上篇继续讲完.如果使用了多种数据访问技术,来访问关系型数据库,则可能希望在这些不同技术所执行的操作之间共享事务.下面示例显示了如何在同一事务中执行 ADO.NET SqlCl ...
- Kali~2018安装后的配置
今天,物理机上成功的安装了Kali Linux系统,但是要想用的顺手还需要花费许多时间和精力,下面就是我对它的养成之路. 一.添加普通用户 useradd -m -G sudo,video,audio ...
- [ SSH框架 ] Spring框架学习之一
一.Spring概述 1.1 什么是Spring Spring是一个开源框架, Spring是于2003年兴起的一个轻量级的Java开发框架,由 Rod Johnson在其著作 Expert One- ...
- kubernetes系列07—Pod控制器详解
本文收录在容器技术学习系列文章总目录 1.Pod控制器 1.1 介绍 Pod控制器是用于实现管理pod的中间层,确保pod资源符合预期的状态,pod的资源出现故障时,会尝试 进行重启,当根据重启策略无 ...
- -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中
本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait( ...
- 强大的jupyter,python开发者的福音
jupyter是一种交互式计算和开发环境的笔记,ipython命令行比原生的python命令行更加友好和高效,还可以运行web版的界面,支持多语言,输出图形.音频.视频等功能. 一.安装 pip3 i ...
- Spring Boot 2.x (十二):Swagger2的正确玩儿法
Swagger2简介 简单的来说,Swagger2的诞生就是为了解决前后端开发人员进行交流的时候API文档难以维护的痛点,它可以和我们的Java程序完美的结合在一起,并且可以与我们的另一开发利器Spr ...
- 痞子衡嵌入式:飞思卡尔Kinetis系列MCU启动那些事(11)- KBOOT特性(ROM API)
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔Kinetis系列MCU的KBOOT之ROM API特性. KBOOT的ROM API特性主要存在于ROM Bootloader ...
- nginx部署dotnet core站点
步骤 aspnetcore程序端口号5001,实际外部端口号8001,相当于把8001收到的请求转发给5001. 把发布出来的文件全部丢掉 /var/www/JuXiangTou 里面去.可以用scp ...