当前位置: 首页 > 科技观察

用测试金字塔来指导数据应用的测试

时间:2023-03-17 15:07:13 科技观察

由于数据应用的开发和功能软件系统的开发有很大的区别,在我们的实践中,开发人员和质量保证人员之间经常会有很多讨论关于如何实施测试。下面将尝试归纳数据应用开发的特点,并探讨在这些特点下应该采用什么样的相应测试策略。功能软件的测试首先,让我们回顾一下功能软件系统开发中的测试。测试一般分为自动化测试和手动测试。由于对人工测试的高度依赖,如果主要依靠人工测试来保证软件质量,将无法满足软件快速迭代上线的需求。现代软件开发越来越强调自动化测试的作用,这也是敏捷软件开发的基本要求。全方位的自动化测试保障,每周上线、每天上线,甚至随时上线成为可能。这里主要讨论自动化测试。测试金字塔我们一般会按照测试金字塔的以下原则来组织自动化测试。测试金字塔分为三层,从下到上分别对应单元测试、集成测试、端到端测试。单元测试是指在函数或类级别对小规模代码进行测试,一般不依赖于外部系统(可以通过Mock或测试替身等方式实现)。单元测试的特点是运行速度非常快(最好全部在内存中),因此执行此类测试的成本也很低。单元测试位于测试金字塔的底部,占据的面积最大。这引导我们大量构建此类测试,并专注于此类测试以确保软件质量。集成测试是比单元测试具有更高集成度的测试。它在运行时执行更广泛的代码路径,通常依赖于数据库和文件系统等外部环境。由于对外部环境的依赖,集成测试运行得更慢并且执行成本更高。集成测试位于测试金字塔的中间,它指导我们构建适度数量的此类测试。集成测试在Web应用场景中也常被称为服务测试(ServiceTest)或API测试。端到端测试是比集成测试更向后的测试,通常通过直接模拟用户操作来构建。因为它需要模拟用户操作,往往需要依赖一套完整的集成环境,所以它的运行速度也是最慢的。端到端测试在Web应用程序场景中通常也称为UI测试。端到端测试位于测试金字塔的顶端,它指导我们构建少量此类测试。测试范围很广,实施方式也很灵活。重点在哪里?我们要去哪里发力?测试金字塔为我们指明了方向。进入测试金字塔为了更好地理解如何进行通用软件测试,我们需要进一步分析测试金字塔。测试带来的信心上面的金字塔图没有体现出一个特点,就是测试的层次越高,给团队带来的信心越强。这个还是比较容易理解的,试想一下,如果没有单元测试,只有端到端测试,我们能不能认为大部分程序还能正常工作(可能会出现一些边界场景的问题)?但是如果只有单元测试,没有端到端的测试,我们连程序能不能跑起来都不知道!端到端测试可以带来极大的信心,但这通常会带来另一个陷阱。由于端到端测试对团队的吸引力太大,有些团队可能会选择直接构建大量的端到端测试而忽略单元测试。这些端到端测试运行缓慢,通常难以修改,并且很快就会让团队不堪重负。缓慢的测试导致缓慢的持续集成,并且高频推出慢慢变得遥不可及。单元测试虽然不能直接给人很强的信心,但它往往是一种更有效的测试方法,因为它可以轻松覆盖各种边界场景。测试金字塔是敏捷软件开发提倡的测试原则。是测试带来的信心和测试本身的可维护性之间的选择。测试金字塔可以指导我们构建足够多的测试,让团队对软件的质量有足够的信心,而不需要过多的测试维护负担。既然是trade-off,那我们可不可以把精力放在单元测试和集成测试上,根本不搭建端到端测试(此时端到端测试的功能是靠人工测试完成的)?测试集成对于一些没有UI(或GUI)的应用,或者一些程序库、框架(如Spring),很多时候测试金字塔中的三类测试并不直接适用。我们可以这样理解:测试金字塔不仅仅是三层,它更多的是帮助我们确立项目中组织测试的原则。其实对于一般的软件测试,我们可以理解为有一个集成的属性。沿着金字塔往上走,测试的集成度越高(它越依赖外部组件)。由于集成度越高,测试过程中需要运行的代码也越来越复杂,测试运行时间越长,测试构建和维护的成本就越高。在实践中,为了提高软件质量和可维护性,我们应该构建更多集成度较低的测试。有了测试集成的理解,我们可以知道,其实金字塔不需要三层,可以是两层,也可以是四层,也可以是五层。这取决于我们如何定义某种类型测试的范围。同时我们也可以知道,其实单元测试、集成测试和端到端测试之间并没有特别明显的界限。接下来,我们从测试集成的角度来看如何构建单元测试。上面说了,测试最好通过Mock或者testdouble来实现,这样不依赖于外部系统。但是如果testmock或者testdouble很难构建,或者构建之后发现测试代码和产品代码很耦合,这时候怎么办?一种可能的选择是考虑使用更高的集成测试。Spark程序就是一个例子。一旦我们使用Spark的DataFrameAPI写代码,我们就很难通过mockSpark的API或者构造一个Spark测试替身来写测试了。这个时候测试只能退而求其次,选择集成度更高的测试,比如在本地启动一个Spark环境,然后在这个环境中运行测试。此时,上面的测试属于什么测试呢?如果我们从三层测试金字塔的测试划分来看问题,很难给这样的测试一个准确的定位。但是,通常我们不需要考虑这样的分类,而是可以把它当作一个低集成度的测试,也就是金字塔底层的测试。如果团队成员能达成一致,我们就可以称之为单元测试,如果不能,称之为Spark测试也不是没有可能。什么时候停止测试那么,对于一般的软件测试,我们可以认为测试策略应该符合一般意义上的金字塔。金字塔的细节,比如应该有多少层,每一层的范围应该是什么,每一层应该使用什么样的测试技术等等,这些问题需要根据具体情况来决定。一般在讨论软件测试时,需要注意软件测试何时停止,即如何判断软件测试是否足够?在老马的《重构 第二版》中,有一个关于什么时候停止测试的观点:有一些测试规则建议,会尽量保证我们测试所有的组合。虽然这些建议值得知道,但在实践中我们需要适度停下来,因为测试达到一定水平后,其边际效用递减。如果我们写了太多的测试,我们可能会因为工作量而气馁。我们应该关注最容易出错的地方,最不自信的地方。一些测试指标,比如覆盖率,可以在一定程度上衡量测试是否全面有效,但最好的衡量方法可能来自于主观感受。如果我们对代码更有信心,则意味着我们的测试进行得很好。不错。主观置信度指标可能是衡量测试充分性的重要参考。如果我们问测试是否足够,我们会问自己是否有信心软件能正常工作。在实践的过程中,我们也可以尝试去分析每一个bug的原因。如果大多数错误是由于代码的测试覆盖率不足引起的,那么此时我们可能应该编写更多的测试。但如果是其他原因造成的,如需求分析不充分或场景设计不完善等,则应在相应阶段加强,而不是盲目增加测试。数据应用测试有了前面对测试策略的分析,我们再来看看数据应用的测试策略。数据应用与功能软件有很大区别,但数据应用也属于一般意义上的软件。数据应用有哪些特点,应该如何进行针对性测试?下面就这些问题进行讨论。根据上一篇文章的分析,数据应用中的代码大致可以分为四类:基础框架(如增强型SQL执行器)、基于SQL的ETL脚本、SQL自定义函数(udf)、数据工具(as上面提到的DWD建模工具)。基础框架测试基础框架代码是数据应用的核心代码。不仅逻辑复杂,在生产运行过程中还需要支持大量的ETL操作。没有人愿意提交一个有问题的基础框架代码,导致大规模的ETL操作失败。因此,我们要高度重视基础框架的测试,保证这部分代码的高质量。基本框架的代码通常用Python或Scala编写。由于Python和Scala语言都有很好的测试支持,对我们做测试是非常有利的。基本框架的另一个特点是它通常没有GUI。根据测试金字塔的原则,我们应该建立更多的低集成测试(以下简称单元测试)和少量的高集成测试(以下简称集成测试)。例如,在上一篇文章中,我们增强了SQL的语法,并添加了新的语法元素,例如变量、函数和模板。通过基础框架实现运行时的变量替换、函数调用等功能。这部分功能逻辑比较复杂,需要建立更多的单元测试和少量的集成测试。ETL脚本的测试ETL脚本的测试可能是数据应用中最大的难点。使用部分集成的测试ETL脚本一般都是基于SQL实现的。SQL本身是一个高度可定制的DSL,就像XML配置一样。应该如何测试XML?许多团队可能会简单地忽略这种类型的测试。但是用SQL写的ETL代码有时可以达到几百行的规模,逻辑比较多,不经过测试很难让人有信心。如何测试?如果我们使用基于Mock的方式来编写测试,我们会发现测试代码和产品代码是一样的。所以,这样做意义不大。如果我们采用高度集成的测试方法(以下简称集成测试),即运行ETL并比较结果,我们会发现编写和维护测试的成本很高。由于ETL脚本代码本身可能比较简单且容易出错,因此没有必要为容易出错的代码编写测试,更何况编写和维护测试的成本也比较高。这使得集成测试的实践显得事倍功半。可以在这里举一个例子。比如一条对分组求和排序输出的SQL,它的代码可能如下图所示。如果我们准备输入数据和输出数据,考虑到各种数据场景的组合,我们可能会花费很多时间,带来很高的测试编写成本。另外,当我们要修改SQL的时候,还要修改测试,带来维护成本。当我们要运行这个测试时,我们必须完成创建表、写入数据、运行脚本和比较结果的整个过程。这些过程都依赖于外部系统,导致测试运行缓慢。这也是维护成本高的一个体现。可见这两种测试方法都不是好的测试方法。测试构建原则那么有什么好的原则吗?我们从实践中总结了一些有价值的想法供大家参考:(1)将ETL脚本分为简单ETL和复杂ETL(可以用代码行数和数据过滤条件的多少来衡量)。简单的ETL通过代码审查或结对编程确保代码质量,无需自动化测试。复杂的ETL通过构建集成测试来确保质量。(2)由于集成测试运行缓慢,可以考虑:尽量减少用例数量,将多个用例合并为一个运行(主要是可以将数据合并为一组数据运行),将测试分类为频繁运行测试和不需要经常运行的测试,例如测试可以分级为P0-P5,P3-P5是需要经常运行的测试(例如每天或每次代码提交),以及P0-P2可以低频率(比如每周)运行,用于开发测试支持工具,让运行时尽可能脱离缓慢的集群环境。例如使用Spark读写本地表(3)考虑使用自定义函数实现复杂逻辑,降低ETL脚本的复杂度。为自定义函数构建完整的单元测试。(4)将复杂的ETL脚本拆分成多个简单的ETL脚本,降低单个ETL脚本的复杂度。加深对业务和数据的理解我们在实践中发现,ETL脚本的问题很多时候不是代码写错了,而是对业务和数据没有很好的理解。比如上一篇空调销售的例子,如果我们在统计销量的时候不知道其他门店退货或者调货的实际业务情况,那么我们就不知道有一些字段在能够反映该业务的数据,即Sales无法正确计算。要形成对数据的深入理解,需要业务知识的长期积累和对数据的长期探索和分析(业务系统通常会经历长期的发展,其间业务规则的复杂度不断增加,导致复杂数据增加性)。对于刚加入团队的新人来说,由于没有考虑到某些业务情况,更容易出现数据的误算。只有深入了解业务和数据,才能开发出高效优质的ETL脚本。有没有什么好的实用方法可以帮助我们加深理解呢?以下几点是我们在实践中总结出来的值得参考的建议:通过思维导图/流程图梳理复杂的业务流程(或业务知识),形成知识库,尽可能进行数据探索,探索业务领域容易被忽视的知识,通过第一步记录下来,与业务系统团队沟通,发现更多的领域业务知识,通过第一步记录下来。如果条件允许,可以在领域内更频繁地使用业务系统,总结更多的领域业务知识,并通过第一步进行记录。针对第一步收集到的这些容易被忽略的特定领域的业务流程,设计自动化测试用例来测试SQL自定义函数的覆盖率。在基于Hadoop的分布式数据平台环境中,SQL自动定义函数通常用Python或Scala编写。由于这些代码通常很少有外部依赖,通常只根据输入数据计算输出数据,因此很容易为这些代码建立测试。事实上,我们可以轻松实现100%的测试覆盖率。在组织测试时,我们可以使用单元测试而不依赖于计算框架。比如下面这个Scala写的自定义函数:在为其创建测试时,可以直接测试内部转换函数array_join_f,一些示例测试场景如:创建单元测试后,一般需要考虑创建一个小数集成测试,即通过Spark框架运行SQL来测试这个自定义函数,举个例子:如果自定义函数本身很简单,我们也可以直接通过Spark测试,覆盖所有场景。从上面的讨论可以看出,SQL自定义函数很容易测试。SQL自定义函数除了好测试之外,还有很多好的特性,比如降低ETL的复杂度,容易重用。因此,我们应该尽量通过自定义函数来封装复杂的业务逻辑。这也是业界数据开发建议的做法(大多数数据开发框架都对自定义函数提供了很好的支持,比如HivePrestoClickHouse等,大多数ETL开发工具也支持自定义函数的开发)。数据工具的测试数据工具示例可以参考文章《数据仓库建模自动化》和?。这些工具的一大特点是它们旨在支持ETL开发,并且仅在开发过程中使用。由于它们不是在生产环境中运行的代码,我们可以降低对它们的质量要求。这些工具通常只是开发者为了提高开发效率而编写的代码,存在较大修改和重构的可能。因此,没有必要过早地建立一个更完整的测试。在我们的实践中,这种代码通常测试很少,我们只针对那些特别复杂的地方构建单元测试,并且没有信心它们能正常工作。如果工具代码是用TDD方式写的,通常会有更多的测试。在持续集成管道中运行测试前面我们讨论了如何针对数据应用程序编写测试,关于测试的另一个重要主题是如何在持续交付管道中运行这些测试。在一个功能性软件项目中,如果我们按照测试金字塔的三个层次来组织测试,那么在流水线中一般会有三个测试流程。从上面的讨论可以看出,数据应用的测试被纵向分成了四行,如何对应pipeline呢?如果我们使用同一个代码库来管理所有的代码,我们可以考虑直接将流水线分成四个并行的进程,分别对应这四行。如果是不同的代码库,可以考虑为不同的代码库建立不同的流水线。在每个管道内部,管道任务可以以单元测试、低集成测试和高集成测试的形式组织。1.独立的ETLpipeline对于ETL代码的测试,有一个问题值得思考。也就是说,ETL脚本通常是非常独立的,彼此之间没有依赖关系。这是因为ETL代码通常是由完整的特定领域语言SQL开发的。与用Python或Scala等通用编程语言编写的代码不同,SQL文件之间不存在依赖关系(如果存在依赖关系,也是通过数据库表产生依赖)。既然如此,假设我们修改了某个ETL文件的代码,是否可以不运行其他ETL文件的测试呢?其实不仅如此,我们甚至可以单独部署这个ETL,而不是一下子部署所有的ETL。这也在一定程度上降低了部署代码的风险。有了以上发现,我们可能不得不重新思考数据应用的持续交付流水线组织形式。一种可能的做法是为每个ETL文件建立一个管道来完成测试和部署的任务。此时,每个ETL都可以理解为一个独立的小程序。这样的想法在实践中并不容易实现,因为它会导致存在大量的流水线(通常是数百条),这给流水线工具带来了很大的压力。常用的流水线工具,如Jenkins,在设计上很难支持如此大规模流水线的创建和管理。如何支持上面的ETL流水线?它可能需要我们开发额外的管道工具。2、云服务中的ETL管道一些云服务厂商正在尝试这样做。他们通常提供基于网络的ETL开发工具,以及用于编写和测试当前ETL的工具。至此,ETL开发者可以在一处完成开发、测试、上线,提高开发效率。这种服务的一个共同缺点是它试图用一个单一的网络系统来支持所有的ETL开发过程,这带来了很多复杂的配置。这实际上将ETL开发过程的复杂性转化为配置的复杂性。与编写代码相比,大多数开发人员不喜欢这种工作方式。(现在的软件开发提倡EverthingasCode方式,试图把开发过程中的所有东西都编码,这样可以更好的利用成熟的代码编辑器,版本管理等功能。web配置的方式也是一样的EverthingasCode的方向相反。)对于这些数据云服务厂商提供的数据开发服务,如果能同时通过代码和Web界面配置来支持数据开发,将会受到更多开发者的喜爱.这在我看来是一个很好的发展方向。小结由于数据应用开发具有很强的独特性(如基于SQL、支持工具多等),其测试与功能型软件开发的测试也有很大区别。本文分析了如何在测试金字塔的指导下制定测试策略。测试金字塔不仅可以很好地指导功能性软件开发,而且经过一般推广后也可以很容易地获得通用的软件测试策略。关于测试金字塔,本文分析了测试带来的质量置信度和测试集成度。这两个概念可以帮助我们更好地理解测试金字塔背后的指导原则。最后,结合我们的实践经验,给出了数据应用中的一些测试构建实践。将数据应用分成四个不同的模块分别构建测试,可以很好地满足数据应用中的质量要求,同时保证更好的可维护性。最后,我们讨论了如何在持续集成流水线中设计测试任务,留下了一个探索的方向,即如何为单个ETL构建流水线。数据应用的质量保证并不容易实现,往往需要我们做出很多取舍,才能找到最合适的方式。解决这个问题,需要充分发挥团队中每个人的积极性,多总结多思考。原文链接:使用测试金字塔指导数据应用的测试——ThoughtworksInsights