This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
<audio id="audio" title="35 | 如何准备测试数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fe/60/fecd1dbbcf1ce96bf9e5e48ef8684860.mp3"></audio>
你好,我是茹炳晟,今天我和你分享的主题是:如何准备测试数据。
从今天开始,我们将一起进入测试数据准备这个新的系列了。我会用四篇文章,和你详细探讨软件测试过程中关于测试数据准备的话题。我会依次分享测试数据创建的基本方法、测试数据准备的痛点、自行开发的测试数据工具,以及目前业内最先进的统一测试数据平台。
你我都非常清楚测试数据的准备是软件测试过程中非常重要的一个环节无论是手工测试还是自动化测试无论是GUI测试还是API测试无论是功能测试还是性能测试都避不开测试数据准备的工作。
所以,如果你想要成长为一名优秀的测试工程师,那就非常有必要深入理解测试数据准备的方法,以及它们各自的优缺点、适用场景了。
今天,我们就先从测试数据准备的基本方法开始吧。
从创建测试数据的维度来看,测试数据准备方法主要可以分为四类:
- 基于GUI操作生成测试数据
- 通过API调用生成测试数据
- 通过数据库操作生成测试数据;
- 综合运用API和数据库的方式生成测试数据。
这时相信你已经回想起我曾在第15篇文章[《过不了的坎聊聊GUI自动化过程中的测试数据》](https://time.geekbang.org/column/article/12399)中从创建测试数据的维度和你分享过这些内容这次的分享只不过是多了“通过GUI调用生成测试数据”的方法。
其实我在第15篇文章的分享内容只是简单的介绍了GUI测试数据准备的方法并没有详细展开。事后你可能也感觉不太过瘾想知道一些更深入、更细节、更贴近业务场景的测试数据准备的知识。所以也就有了我今天的这次分享。
那么,接下来我们就赶紧开始吧,一起聊聊这四种测试数据准备的方法。
## 基于GUI操作生成测试数据
基于GUI操作生成测试数据是最原始的创建测试数据的方法。简单地说它就是采用E2E的方式来执行业务场景然后生成数据的方法。
比如你想要测试用户登录功能那么首先就要准备一个已经注册的用户为此你可以直接通过GUI界面来注册一个新用户然后用这个新创建的用户完成用户登录功能的测试。
这个方法的优点是简单直接,在技术上没有任何复杂性,而且所创建的数据完全来自于真实的业务流程,可以最大程度保证数据的正确性。但是,该方法的缺点也十分明显,主要体现在以下这四个方面:
<li>
**创建测试数据的效率非常低**。一是因为每次执行GUI业务操作都只能创建一条数据二是因为基于GUI操作的执行过程比较耗时。
</li>
<li>
**基于GUI的测试数据创建方法不适合封装成测试数据工具**。由于测试数据的创建是通过GUI操作实现的所以把这种数据创建方法封装成测试数据准备工具的过程其实就是在开发GUI自动化测试用例。无论是从开发工作量还是从执行效率来讲把基于GUI操作的测试数据创建方法封装成测试数据准备工具都不是最佳的选择。
</li>
<li>
**测试数据成功创建的概率不会太高**。因为测试数据准备的成功率受限于GUI自动化执行的稳定性而且任何界面的变更都有可能引发测试数据创建的失败。
</li>
<li>
**会引入不必要的测试依赖**。比如你的被测对象是用户登录功能通过GUI页面操作准备这个已经注册的用户就首先要保证用户注册功能没有问题而这显然是不合理的。
</li>
鉴于以上四方面的原因在实际的测试过程中我们很少直接使用基于GUI的操作生成测试数据。只有在万不得已的情况下比如没有其他更好的方式可以创建正确可靠的测试数据时我们才会使用这个方法。
而且这里我需要说明的是基于GUI操作生成测试数据的方法一般只用于手工测试因为自动化测试中使用这种数据准备方法基本相当于要开发一个完整的GUI自动化测试用例代价太大。
那我为什么还要介绍这个方法呢其实这个方法更重要的应用场景是帮助我们找到创建一个测试数据的过程中后端调用了哪些API以及修改了哪些数据库的业务表是“通过API调用生成测试数据”以及“通过数据库操作生成测试数据”这两种方法的基础。
## 通过API调用生成测试数据
通过API调用生成测试数据是目前主流的测试数据生成方法。其实当我们通过操作GUI界面生成测试数据时实际的业务操作往往是由后端的API调用完成的。所以我们完全可以通过直接调用后端API生成测试数据。
还是以用户登录功能的测试为例我们通过GUI界面注册新用户时实际上就是调用了createUser这个API。既然知道了具体要调用哪个API那么我们就可以跳过在GUI界面的操作直接调用createUser生成“已经注册的用户”这个测试数据了。
**为了规避在创建测试数据时过于在乎实现细节的问题在实际工程实践中我们往往会把调用API生成测试数据的过程封装成测试数据准备函数**。那问题是我怎么才能知道前端新用户注册这个操作到底调用了哪些后端API呢这里我推荐三种方式
<li>
直接询问开发人员,这是最直接的方法;
</li>
<li>
如果你有一定的代码基础,可以直接阅读源代码,这个方法也可以作为直接询问方法的补充;
</li>
<li>
在一个你可以独占的环境上执行GUI操作创建测试数据与此同时监控服务器端的调用日志分析这个过程到底调用了哪些API。
</li>
通过API调用生成测试数据的方法优点主要体现在以下几个方面
- 可以保证创建的测试数据的准确性原因是使用了和GUI操作同样的API调用
- 测试数据准备的执行效率更高因为该方法跳过了耗时的GUI操作
- 把创建测试数据的API调用过程封装成测试数据函数更方便因为这个调用过程的代码逻辑非常清晰
- 测试数据的创建可以完全依赖于API调用当创建测试数据的内部逻辑有变更时由于此时API内部的实现逻辑也会由开发人员同步更新所以我们依旧可以通过调用API来得到逻辑变更后的测试数据而这个过程对使用来说是完全透明的。
但是,该方法也不是完美无瑕的,其缺点主要表现在:
<li>
并不是所有的测试数据创建都有对应的API支持。也就是说并不是所有的数据都可以通过API调用的方式创建有些操作还是必须依赖于数据库的CRUD操作。那么这时我们就不得不在测试数据准备函数中加入数据库的CRUD操作生成测试数据了。
</li>
<li>
有时创建一条业务线上的测试数据往往需要按一定的顺序依次调用多个API并且会在多个API调用之间传递数据这也无形中增加了测试数据准备函数的复杂性。
</li>
<li>
虽然相比于GUI操作方式基于API调用的方式在执行速度上已经得到了大幅提升并且还可以很方便地实现并发执行比如使用JMeter或者Locust但是对于需要批量创建海量数据的场景还是会力不从心。
</li>
因此业界往往还会通过数据库的CRUD操作生成测试数据。
## 通过数据库操作生成测试数据
通过数据库操作生成测试数据,也是目前主流的测试数据生成方法。这个方法的实现原理很简单,就是直接通过数据库操作,将测试数据插入到被测系统的后台数据库中。
**常见的做法是将创建数据需要用到的SQL语句封装成一个个的测试数据准备函数当我们需要创建数据时直接调用这些封装好的函数即可。**
还是以用户登录功能测试为例当我们通过GUI界面注册新用户时实际上是在后端调用了createUser这个API而这个API的内部实现逻辑是将用户的详细信息插入到了userTable和userRoleTable这两张业务表中。
那么此时我们就可以直接在userTable和userRoleTable这两张业务表中插入数据然后完成这个新用户的注册工作。
**这样做的前提是你需要知道前端用户通过GUI操作注册新用户时到底修改了哪些数据库的业务表**。这里,我也推荐三种方式:
<li>
直接向开发人员索要使用到的SQL语句
</li>
<li>
直接阅读产品源代码;
</li>
<li>
在一个你可以独占的环境上执行GUI操作产生测试数据与此同时监控独占环境的数据库端业务表的变化找到哪些业务表发生了变化。
</li>
通过数据库操作生成测试数据的方法,主要优点是测试数据的生成效率非常高,可以在较短的时间内创建大批量的测试数据。
当然,这个方法的缺点也非常明显,主要体现在以下几个方面:
- 很多时候,一个前端操作引发的数据创建,往往会修改很多张表,因此封装的数据准备函数的维护成本要高得多;
- 容易出现数据不完整的情况,比如一个业务操作,实际上在一张主表和一张附表中插入了记录,但是基于数据库操作的数据创建可能只在主表中插入了记录,这种错误一般都会比较隐蔽,往往只在一些特定的操作下才会发生异常;
- 当业务逻辑发生变化时即SQL语句有变化时需要维护和更新已经封装的数据准备函数。
## 综合运用API和数据库的方式生成测试数据
目前在实际的工程实践中很少使用单一的方法生成测试数据基本都是采用API和数据库相结合的方式。**最典型的应用场景是先通过API调用生成基础的测试数据然后使用数据库的CRUD操作生成符合特殊测试需求的数据**。所以你经常会看到很多的数据准备函数中既有API操作又有数据库操作。
我以创建用户为例和你分享一下如何综合运用API和数据库两种方式创建测试数据吧。
假设我们需要封装一个创建用户的函数这个函数需要对外暴露“用户国家”和“支付方式”这两个参数。由于实际创建用户是通过后台createUser API完成的但是这个API并不支持指定“用户国家”和“支付方式”所以我们就需要自己封装一个创建用户的函数。
自己封装用户创建函数的方法,你可以通过下面这个思路实现:
- 首先调用createUser API完成基本用户的创建
- 然后调用paymentMethod API实现用户对于不同支付方式的绑定其中paymentMethod API使用的userID就是上一步中createUser API产生的用户ID
- 最后通过数据库的SQL语句更新“用户国家”。
在这个例子中createUser API和paymentMethod API只是为了说明如何综合运用API的顺序调用而其具体参数并不是我要阐述的关键内容所以我并没有和你详细说明这两个API的参数、实现方式等问题。另外我在最后一步综合运用了数据库的CRUD操作完成了创建测试数据的全部工作。
这,就是一个封装测试数据准备函数的典型例子了。
## 总结
今天,我从测试数据创建的角度,和你分享了准备测试数据的四种方法。
其中基于GUI操作生成测试数据是最原始的方法但是效率很低而且会引入不必要的依赖通过API调用以及数据库操作的方式生成测试数据是目前主流的做法通过API调用的方式具有数据准确度高但是创建效率较低的特点而通过数据库的方式具有创建效率高但是维护复杂度也高的特点。
所以在实际项目中业界往往会综合采用API和数据库的方式生成测试数据即通过API调用生成基础数据然后使用数据库的CRUD操作进一步生成符合特殊测试需求的数据。
## 思考题
目前,我们需要创建的测试数据并不仅仅局限于数据库,很多时候还需要创建消息队列里面的数据。你在实际工作中遇到过这类测试数据吗?你又是如何处理的呢?
感谢你的收听,欢迎你给我留言。

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="36 | 浅谈测试数据的痛点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/c7/7eb9c1fe4cfac02a086dc158ee636fc7.mp3"></audio>
你好,我是茹炳晟。今天我和你分享的主题是:浅谈测试数据的痛点。
在上一篇文章中我和你分享了创建测试数据的四大类方法即基于GUI操作生成测试数据、通过API调用生成测试数据、通过数据库操作生成测试数据以及综合运用API和数据库的方式生成测试数据。
但是,我并没有谈到应该在什么时机创建这些测试数据。比如,是在测试用例中实时创建测试数据,还是在准备测试环境时一下子准备好所有的测试数据呢。
其实,在不同的时机创建测试数据,就是为了解决准备测试数据的不同痛点。那么,准备测试数据的痛点,都体现在哪些方面呢?
- 在测试用例执行过程中,创建所需的数据往往会耗时较长,从而使得测试用例执行的时间变长;
- 在测试执行之前,先批量生成所有需要用到的测试数据,就有可能出现在测试用例执行时,这些事先创建好的数据已经被修改而无法正常使用了的情况;
- 在微服务架构下,测试环境本身的不稳定,也会阻碍测试数据的顺利创建。
那么,今天我们就先来聊聊与测试数据创建时机相关的话题。
从测试数据创建的时机来看主要分为On-the-fly实时创建和Out-of-box事先创建测试数据两类方法。这两类方法都有各自的优缺点以及适用的最佳场景。而且在工程实践中我们往往会综合使用这两种方法。
接下来我先和你分别介绍一下这两类方法。其实这两类方法我已经在第15篇文章[《过不了的坎聊聊GUI自动化过程中的测试数据》](https://time.geekbang.org/column/article/12399)中提到过了。但是,当时我只是笼统地和你分享了这两类方法的概念,并没有详细展开讨论。所以,我今天就会通过一些实例,和你更加详细地讨论这两类方法。
## On-the-fly
On-the-fly方法又称实时创建方法指的是在测试用例的代码中实时创建要使用到的测试数据。比如对于用户登录功能的测试那么在测试用例开始的部分首先调用我在上一篇文章中介绍的创建新用户的数据准备函数来生成一个新用户接下来的测试将会直接使用这个新创建的用户。
对于On-the-fly测试用例中所有用到的测试数据都在测试用例开始前实时准备。**采用On-the-fly方式创建的数据都是由测试用例自己维护的不会依赖于测试用例外的任何数据从而保证了数据的准确性和可控性最大程度地避免了出现“脏”数据的可能。**
那到底什么是“脏”数据呢?这里的“脏”数据是指,数据在被实际使用前,已经被进行了非预期的修改。
从理论上来讲,这种由自己创建和维护数据的方式,是最佳的处理方式,很多早期的测试资料都推荐采用这种方式。但是,随着软件架构的发展,以及软件发布频率的快速增长,这种方式的弊端越来越明显,主要体现在以下三方面:
**首先,实时创建测试数据比较耗时**。在测试用例执行的过程中实时创建测试数据,将直接导致测试用例的整体执行时间变长。
我曾统计过一个大型电商网站的测试用例执行时间总的测试用例执行时间中有30%-40%的时间花在了测试数据的实时准备上,也就是说测试数据的实时准备花费了差不多一半的测试用例执行时间。
对传统软件企业来说,它们可能并不太在意这多出来的测试执行时间,因为它们的软件发布周期比较长,留给测试的时间也比较长,所以这多出来的时间可以忽略不计。
但是,对于互联网软件企业来说,它们的软件发布频率很高,相应地留给测试执行的时间也都很短,那么缩短测试数据的准备时间的重要性就不言而喻了。
要解决创建测试数据耗时的问题除了从测试数据准备函数的实现入手外还可以考虑采用我后面要介绍的事先创建测试数据Out-of-box的方式。
**其次,测试数据本身存在复杂的关联性**。很多时候你为了创建一个你需要使用的业务数据,往往需要先创建一堆其他相关联的数据,越是业务链后期的数据,这个问题就越严重。
比如创建订单数据这个最典型的案例。由于创建订单的数据准备函数需要提供诸如卖家、买家、商品ID等一系列的前置数据所以你就不得不先创建出这些前置数据。这样做一方面测试数据准备的复杂性直线上升另一方面创建测试数据所需要的时间也会变得更长。
为了缓解这个问题你可以考虑将部分相对稳定的数据事先创建好而不要采用On-the-fly的方式去创建所有的数据。
**最后一个问题来自于微服务架构的调整**。早期的软件架构都是单体的只要测试环境部署成功了那么所有的功能就都可以使用了。而现如今大量的互联网产品都采用了微服务架构所以很多时候测试环境并不是100%处于全部可用的状态。也就是说,并不是所有的服务都是可用的,这就给测试数据准备带来了新的挑战。
比如你为了测试用户登录功能根据On-the-fly的策略你首先需要创建一个新用户。假设在微服务架构下注册用户和用户登录隶属于两个不同的微服务而此时注册用户的微服务恰好因为某种原因处于不可用状态那么这时你就无法成功创建这个用户也就是无法创建测试数据。因此整个测试用例都无法顺利执行显然这不是我们想要的结果。
为了解决这个问题你可以采用事先创建数据Out-of-box的方式只要能够保证测试环境在某个时间段没有问题那么就可以在这个时间段事先创建好测试数据。
为了解决上述三个问题Out-of-box即事先创建测试数据的方式就应运而生了。那么
接下来我们就一起看看这个方式的原理,以及适用的场景吧。
## Out-of-box
Out-of-box方法又称开箱即用方法指的是在准备测试环境时就预先将测试需要用到的数据全部准备好而不是在测试用例中实时创建。因此我们可以节省不少测试用例的执行时间同时也不会存在由于环境问题无法创建测试数据而阻碍测试用例执行的情况。也就是说Out-of-box方法可以克服On-the-fly方法的缺点那么这个方式又会引入哪些致命的新问题呢
**Out-of-box最致命的问题是“脏”数据。**
比如,我们在测试用例中使用事先创建好的用户进行登录测试,但这个用户的密码被其他人无意中修改了,导致测试用例执行时登录失败,也就不能顺利完成测试了。那么,此时这个测试用户数据就成为了“脏”数据。
再比如,我们在测试用例中使用事先创建的测试优惠券去完成订单操作,但是由于某种原因这张优惠券已经被使用过了,导致订单操作的失败,也就意味着测试用例执行失败。那么,此时这个测试优惠券数据也是“脏”数据。
由此可见,这些事先创建好的测试数据,在测试用例执行的那个时刻,是否依然可用其实是不一定的,因为这些数据很有可能在被使用前已经发生了非预期的修改。
这些非预期的修改主要来自于以下三个方面:
<li>
其他测试用例使用了这些事先创建好的测试数据,并修改了这些数据的状态;
</li>
<li>
执行手工测试时,因为直接使用了事先创建好的数据,很有可能就会修改了某些测试数据;
</li>
<li>
自动化测试用例的调试过程,修改了事先创建的测试数据;
</li>
为了解决这些“脏”数据我们只能通过优化流程去控制数据的使用。目前业内有些公司会将所有事先创建好的测试数据列在一个Wiki页面然后按照不同的测试数据区段来分配使用对象。
比如假设我们事先创建了1000个测试用户那么用户ID在0001-0200范围内数据给这个团队使用而用户ID在0201-0500范围内的数据则给另一个团队使用。这个分配工作要靠流程保证那么前提就是所有人都要遵守这些流程。
但我一直认为,但凡需要靠流程保证的一定不是最靠谱的,因为你无法确保所有人都会遵守流程。也正是因为这个原因,在实际项目中我们还是会经常看到由“脏”数据引发测试用例执行失败的案例。
更糟糕的是,如果自动化测试用例直接采用硬编码的方式,去调用那些只能被一次性使用的测试数据(比如订单数据、优惠券等)的话,你会发现测试用例只能在第一次执行时通过,后面再执行都会因为测试数据的问题而失败。
所以你还需要在测试用例级别保证测试数据只被调用一次而这往往会涉及到跨测试用例的测试数据维护问题往往实现起来非常麻烦。所以说Out-of-box方法不适用于只能一次性使用的测试数据场景。
## 综合运用On-the-fly和Out-of-box
为了充分利用On-the-fly和Out-of-box这两种方式的各自优点并且规避各自的缺点实际的工程实践中往往是采用综合运用On-the-fly和Out-of-box的方式来实现测试数据的准备的。
**在实际的测试项目中,我们可以根据测试数据的特性,把它们分为两大类,用业内的行话来讲就是“死水数据”和“活水数据”。**
“死水数据”是指那些相对稳定不会在使用过程中改变状态并且可以被多次使用的数据。比如商品分类、商品品牌、场馆信息等。这类数据就非常适合采用Out-of-box方式来创建。
**这里需要特别说明的是,哪些数据属于“死水数据”并不是绝对的,由测试目的决定。**
比如用户数据在大多数的非用户相关的测试用例中基本属于“死水数据”因为绝大多数的业务测试都会包含用户登录的操作而且并不会去修改用户本身的数据属性所以这时我们就可以将用户数据按照“死水数据”处理也就是采用Out-of-box的方式创建。
但是,对于那些专门测试用户账号的测试用例来讲,往往会涉及到用户撤销、激活、修改密码等操作,那么此时的用户数据就不再是“死水数据”了,而应该按照“活水数据”处理。
“活水数据”是指那些只能被一次性使用或者经常会被修改的测试数据。最典型的数据是优惠券、商品本身、订单等类似的数据。这类数据通常在被一次性使用后状态就发生了变化不能反复使用。那么这类测试数据就更适合采用On-the-fly自维护的方式。
同时由于有Out-of-box数据的支持这类数据往往不需要从最源头开始创建而是可以基于已有的Out-of-box数据生成。
比如在使用On-the-fly方式创建订单数据时你可以直接使用Out-of-box的用户数据来作为买家数据。
由此可见,综合运用这两类方法,可以以互补的方式解决测试数据准备的很多痛点,比如测试数据准备比较耗时、测试数据存在“脏”数据的可能,以及测试环境不稳定造成的测试数据无法创建等问题。
## 总结
今天我从测试数据创建时机的角度和你分享了On-the-fly和Out-of-box这两类创建数据的方式。
On-the-fly方法又称为实时创建方法指的是在测试用例的代码中实时创建测试用例所要使用到的测试数据具有数据可靠性高的优点但是会比较耗时。
而Out-of-box方法又称为开箱即用方法指的是在准备测试环境时就事先准备好测试需要用到的全部数据。这样可以有效缩短测试用例的执行时间但是存在“脏”数据的问题。
最后我从“死水数据”和“活水数据”的角度讨论了如何综合运用上述两种方式创建测试数据其中“死水数据”适合用Out-of-box的方式而“活水数据”适合采用On-the-fly的方式。
## 思考题
你所在的项目中,采用的是什么样的测试数据准备策略,这个策略的优缺点是什么?为什么会选择这样的策略呢?另外,你所在团队会使用线上真实的数据进行测试吗?
感谢你的收听,欢迎你给我留言。

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="37 | 测试数据的“银弹”- 统一测试数据平台(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/30/52/3035a661fb2c66f6b41096b5da50ac52.mp3"></audio>
你好,我是茹炳晟。今天我和你分享的主题是:测试数据的“银弹”之统一测试数据平台(上)。
在《如何准备测试数据?》和《浅谈测试数据的痛点》这两篇文章中,我介绍了创建测试数据的主要方法,以及创建测试数据的时机。在此基础上,今天我将和你聊聊全球大型电商企业中关于准备测试数据的最佳实践。
这个主题我会从全球大型电商企业早期的测试数据准备实践谈起和你一起分析这些测试数据准备方法在落地时遇到的问题以及如何在实践中解决这些问题。其实这种分析问题、解决问题的思路也是推动着测试数据准备时代从1.0到2.0再到3.0演进的原因。
所以在这个过程中你可以跟着时代的演进理解测试数据准备技术与架构的发展历程并进一步掌握3.0时代出现的业内处于领先地位的“统一测试数据平台”的设计思路。
因为这个主题的内容相对较多,为了降低你的学习负担、便于理解消化,我把它分成了两篇文章。同时,为了和你深入地讨论这个话题,也可以真正做到“接地气儿”,我会在这两篇文章中列举很多工程中的实际问题,并给出相应的解决方案。或许这些问题你也曾经遇到过,或者正在被其折磨,希望我给出的这些方案,可以给你启发,帮你攻克这些难关。
我们就先从数据准备的1.0时代谈起吧。
## 测试数据准备的1.0时代
其实据我观察目前很多软件企业还都处于测试数据准备的1.0时代。
**这个阶段最典型的方法就是,将测试数据准备的相关操作封装成数据准备函数**。这些相关操作既可以是基于API的也可以是基于数据库的当然也可以两者相结合。
有了这些数据准备函数后你就可以在测试用例内部以On-the-fly的方式调用它们实时创建数据也可以在测试开始之前在准备测试环境的阶段以Out-of-box的方式调用它们事先创建好测试数据。
那么一个典型的数据准备函数长什么样子呢我们一起来看看这段代码吧里面的createUser函数就是一个典型的数据准备函数了。
```
public static User createUser(String userName, String password, UserType userType, PaymentDetail paymentDetail, Country country, boolean enable2FA)
{
//使用API调用的方式和数据库CRUD的方式实际创建测试数据
...
}
```
乍一看,你可能觉得,如果可以将大多数的业务数据创建都封装成这样的数据准备函数,那么测试数据的准备过程就变成了调用这些函数,而无需关心数据生成的细节,这岂不是很简单、直观嘛。
但,真的是这样吗?
这里,我建议你在继续阅读后面的内容之前,先思考一下这个方法会有什么短板,然后再回过头来看答案,这将有助于加深你对这个问题的理解。当然,如果你已经在项目中实际采用了这个方法的话,相信你已经对它的短板了如指掌了。
好了,现在我来回答这个问题。**利用这种数据准备函数创建测试数据方法的最大短板,在于其参数非常多、也非常复杂**。在上面这段代码中createUser函数的参数有6个。而实际项目中由于测试数据本身的复杂性、灵活性参数的数量往往会更多十多个都是很常见的。
而在调用数据准备函数之前你首先要做的就是准备好这些参数。如果这些参数的数据类型是基本类型的话还比较简单比如createUser函数中userName、password是字符串型enable2FA是布尔型但这些参数如果是对象比如createUser函数的userType、paymentDetail和Country就是对象类型的参数的话就很麻烦了。为什么呢
因为,你需要先创建这些对象。更糟糕的是,如果这些对象的初始化参数也是对象的话,就牵连出了一连串的数据创建操作。
下面这段代码就是使用createUser函数创建测试数据的一个典型代码片段。
```
//准备createUser的参数
UserType userType = new UserType(&quot;buyer&quot;);
Country country = new Country(&quot;US&quot;);
//准备createPaymentDetail的参数
PaymentType paymentType = new PaymentType(&quot;Paypal&quot;);
//调用createPaymentDetail创建paymentDetail对象
PaymentDetail paymentDetail = createPaymentDetail(paymentType2000);
//对主要的部分调用createUser产生用户数据
User user=createUser(“TestUser001”, “abcdefg1234”, userType, paymentDetail, country, true)
```
由此可见,每次使用数据准备函数创建数据时,你都要知道待创建数据的全部参数细节,而且还要为此创建这些参数的对象,这就让原本看似简单的、通过数据准备函数调用生成测试数据的过程变得非常复杂。
那么,你可能会问,这个过程是必须的吗,可以用个某些技术手段“跳过”这个步骤吗?
**其实,绝大多数的测试数据准备场景是,你仅仅需要一个所有参数都使用了缺省值的测试数据,或者只对个别几个参数有明确的要求,而其他参数都可以是缺省值的测试数据。**
以用户数据创建为例大多情况下你只是需要一个具有缺省Default参数的用户或者是对个别参数有要求的用户。比如你需要一个美国的用户或者需要一个userType是buyer的用户。这时让你去人为指定所有你并不关心的参数的做法其实是不合理的也没有必要。
为了解决这个问题在工程实践中就引入了如图1所示的封装数据准备函数的形式。
<img src="https://static001.geekbang.org/resource/image/38/65/3868e8373fbf7a9c4892fce778ed6b65.png" alt="" />
在这个封装中我们将实际完成数据创建的函数命名为createUserImpl这个函数内部将通过API调用和数据库CRUD操作的方式完成实际数据的创建工作同时对外暴露了所有可能用到的user参数A、B、C、D、E。
接着我们封装了一个不带任何参数的createDefaultUser函数。函数内部的实现首先会用默认值初始化user的参数A、B、C、D、E然后再将这些参数作为调用createUserImpl函数时的参数。
那么,**当测试用例中仅仅需要一个没有特定要求的默认用户时**你就可以直接调用这个createDefaultUser函数隐藏测试用例并不关心的其他参数的细节此时也就真正做到了用一行代码生成你想要的测试数据。
**而对于那些测试用例只对个别参数有要求的场景**比如只对参数A有要求的场景我们就可以为此封装一个createXXXUser(A)函数用默认值初始化参数B、C、D、E然后对外暴露参数A。
当测试用例需要创建A为特定值的用户时你就可以直接调用createXXXUser(A)函数然后createXXXUser(A)函数会用默认的B、C、D、E参数的值加上A的值调用createUserImpl函数以此完成测试数据的创建工作。
当然,**如果是对多个参数有特定要求的场景**我们就可以封装出createYYYUser这样暴露多个参数的函数。
通过这样的封装对于一些常用的测试数据组合我们通过一次函数调用就可以生成需要的测试数据而对于那些比较偏门或者不常用的测试数据我们依然可以通过直接调用最底层的createUserImpl函数完成数据创建工作。可见这个方法相比之前已经有了很大的进步。
但是,在实际项目中,大量采用了这种封装的数据准备函数后,还有一些问题亟待解决,主要表现在以下几个方面:
<li>
**对于参数比较多的情况,会面临需要封装的函数数量很多的尴尬**。而且参数越多,组合也就越多,封装函数的数量也就越多。
</li>
<li>
**当底层Impl函数的参数发生变化时需要修改所有的封装函数。**
</li>
<li>
<p>**数据准备函数的JAR包版本升级比较频繁**。由于这些封装的数据准备函数往往是以JAR包的方式提供给各个模块的测试用例使用的并且JAR会有对应的版本控制所以一旦封装的数据准备函数发生了变化我们就要升级对应JAR包的版本号。<br />
而这些封装的数据准备函数,由于需要支持新的功能,并修复现有的问题,所以会经常发生变化,因此测试用例中引用的版本也需要经常更新。</p>
</li>
为了可以进一步解决这三个问题同时又可以最大程度地简化测试数据准备工作我们就迎来了数据准备函数的一次大变革由此也将测试数据准备推向了2.0时代。
这里需要强调一下我往往把到目前为止所采用的测试数据实践称为数据准备的1.0时代。我会在下一篇文章中和你详细介绍2.0时代下的测试数据准备都有哪些关键的技术创新,相信一定会让你有眼前一亮的感觉。
## 总结
在1.0时代,准备测试数据最典型的方法就是,将测试数据准备的相关操作封装成数据准备函数。
归纳起来,这个时代的数据准备函数,主要有两种封装形式:
第一种是,直接使用暴露全部参数的数据准备函数,虽说灵活性最好,但是每次调用前都需要准备大量的参数,从使用者的角度来看便利性比较差;
第二种是,为了解决便利性差的问题,我们引入了更多的专用封装函数,在灵活性上有了很大的进步,但是也带来了可维护差的问题。
所以为了可以更高效地准备测试数据我们即将迎来测试数据准备的2.0时代,拭目以待吧。
## 思考题
你所在的团队,是否已经在使用我今天聊到的这些方法了呢,使用过程中还遇到了哪些挑战?如果没有使用这些方法的话,你又是采用什么方法创建测试数据的呢?
感谢你的收听,欢迎你给我留言。

View File

@@ -0,0 +1,131 @@
<audio id="audio" title="38 | 测试数据的“银弹”- 统一测试数据平台(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d2/6d/d2fd309bffc51b7875c4871b6a6c036d.mp3"></audio>
你好,我是茹炳晟,今天我分享的主题是:“测试数据的“银弹”之统一测试数据平台(下)”。
在上一篇文章中我和你分享了测试数据准备1.0时代的实践在这个1.0时代测试数据准备的最典型方法是将测试数据准备的相关操作封装成数据准备函数。今天我将继续为你介绍测试数据准备的2.0和3.0时代的实践,看看创建测试数据的方法,又发生了哪些变革。
在1.0时代,为了让数据准备函数使用更方便,避免每次调用前都必须准备所有参数的问题,我和你分享了很多使用封装函数隐藏默认参数初始化细节的方法。
但是这种封装函数的方式也会带来诸如需要封装的函数数量较多、频繁变更的维护成本较高以及数据准备函数JAR版本升级的尴尬。所以为了系统性地解决这些可维护性的问题我们对数据准备函数的封装方式做了一次大变革也由此进入了测试数据准备的2.0时代。
## 测试数据准备的2.0时代
在测试数据准备的2.0时代数据准备函数不再以暴露参数的方式进行封装了而是引入了一种叫作Builder Pattern生成器模式的封装方式。这个方式能够在保证最大限度的数据灵活性的同时提供使用上的最大便利性并且维护成本还非常低。
事实上如果不考虑跨平台的能力Builder Pattern可以说是一个接近完美的解决方案了。关于什么是“跨平台的能力”我会在测试数据准备的3.0时代中解释这里先和你介绍我们的主角Builder Pattern。
Builder Pattern是一种数据准备函数的封装方式。在这种方式下当你需要准备测试数据时不管情况多么复杂你一定可以通过简单的一行代码调用来完成。听起来有点玄乎没关系看完我列举的这些实例你马上就可以理解了。
**实例一**你需要准备一个用户数据而且对具体的参数没有任何要求。也就是说你需要的仅仅是一个所有参数都可以采用默认值的用户。那么在Builder Pattern的支持下你只需要执行一行代码就可以创建出你需要的这个所有参数都是默认值的用户了。这行代码就是
```
UserBuilder.build();
```
**实例二**你现在还需要一个用户但是这次需要的是一个美国的用户。那么这时在Builder Pattern的支持下你只用一行代码也可以创建出这个指定国家是美国而其他参数都是默认值的用户。这行代码就是
```
UserBuilder.withCountry(&quot;US&quot;).build();
```
**实例三**你又需要这样一个用户数据英国用户支付方式是Paypal其他参数都是默认值。那么这时在Builder Pattern的支持下你依然可以通过一行简单的代码创建出满足这个要求的用户数据。这行代码就是
```
UserBuilder.withCountry(&quot;US&quot;).withPaymentMethod(&quot;Paypal&quot;).build();
```
通过这三个实例你肯定已经感受到相对于1.0时代的通过封装函数隐藏默认参数初始化的方法来说Builder Pattern简直太便利了。
趁热打铁我再来和你总结一下Builder Pattern的便利性吧
- 如果仅仅需要一个全部采用缺省参数的数据的话你可以直接使用TestDataBuilder.build()得到;
- 如果你对其中的某个或某几个参数有特定要求的话,你可以通过“.withParameter()”的方式指定,而没有指定的参数将自动采用默认值。
这样一来,无论你对测试数据有什么要求,都可以以最灵活和最简单的方式,通过一行代码得到你要的测试数据。
在实际工程项目中随着Builder Pattern的大量使用又逐渐出现了更多的新需求为此我归纳总结了以下4点
- 有时候,出于执行效率的考虑,我们不希望每次都重新创建测试数据,而是希望可以从被测系统的已有数据中搜索符合条件的数据;
- 但是,还有些时候,我们希望测试数据必须是全新创建的,比如需要验证新建用户首次登录时,系统提示修改密码的测试场景,就需要这个用户一定是被新创建的;
- 更多的时候,我们并不关心这些测试数据是新创建的,还是通过搜索得到的,我们只希望以尽可能短的时间得到需要的测试数据;
- 甚至还有些场景我们希望得到的测试数据一定是来自于Out-of-box的数据。
为了能够满足上述的测试数据需求我们就需要在Builder Pattern的基础上进一步引入Build Strategy的概念。顾名思义Build Strategy指的是数据构建的策略。
为此我们引入了Search Only、Create Only、Smart和Out-of-box这四种数据构建的策略。这四类构建策略在Builder Pattern中的使用很简单只要按照以下的代码示例指定构建策略就可以了
```
UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.SEARCH_ONLY.build();
UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.CREATE_ONLY).build();
UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.SMART).build();
UserBuilder.withCountry(“US”).withBuildStrategy(BuildStrategy.OUT_OF_BOX).build();
```
结合着这四类构建策略的代码,我再和你分享一下,它们会在创建测试数据时执行什么操作,返回什么样的结果:
- 当使用BuildStrategy.SEARCH_ONLY策略时Builder Pattern会在被测系统中搜索符合条件的测试数据如果找到就返回否则就失败这里失败意味着没能返回需要的测试数据
- 当使用BuildStrategy.CREATE_ONLY策略时Builder Pattern会在被测系统中创建符合要求的测试数据然后返回
- 当使用BuildStrategy.SMART策略时Builder Pattern会先在被测系统中搜索符合条件的测试数据如果找到就返回如果没找到就创建符合要求的测试数据然后返回
- 当使用BuildStrategy.OUT_OF_BOX策略时Builder Pattern会返回Out-of-box中符合要求的数据如果在Out-of-box中没有符合要求的数据build函数就会返回失败
由此可见引入Build Strategy之后Builder Pattern的适用范围更广了几乎可以满足所有的测试数据准备的要求。
但是不知道你注意到没有我们其实还有一个问题没有解决那就是这里的Builder Pattern是基于Java代码实现的如果你的测试用例不是基于Java代码实现的那要怎么使用这些Builder Pattern呢
在很多大型公司测试框架远不止一套不同的测试框架也是基于不同语言开发的比如有些是基于Java的有些是基于Python的还有些基于JavaScript的。而非Java语言的测试框架想要使用基于Java语言的Builder Pattern的话往往需要进行一些额外的工作比如调用一些专用函数等。
我来举个例子吧。对于JavaScript来说如果要使用Java的原生类型或者引用的话你需要使用Java.type()函数而如果要使用Java的包和类的话你就需要使用专用的importPackage()函数 和 importClass() 函数。
这些都会使得调用Java方法很不方便其他语言在使用基于Java的Builder Pattern时也有同样的问题。
但是我们不希望、也不可能为每套基于不同开发语言的测试框架都封装一套Builder Pattern。所以我们就希望一套Builder Pattern可以适用于所有的测试框架这也就是我在前面提到的测试准备函数的“跨平台的能力”了。
为了解决这个问题测试数据准备走向了3.0时代。
## 测试数据准备的3.0时代
为了解决2.0时代跨平台使用数据准备函数的问题我们将基于Java开发的数据准备函数用Spring Boot包装成了Restful API并且结合Swagger给这些Restful API提供了GUI界面和文档。
这样一来我们就可以通过Restful API调用数据准备函数了而且由于Restful API是通用接口所以只要测试框架能够发起http调用就能使用这些Restful API。于是几乎所有的测试框架都可以直接使用这些Restful API准备测试数据。
由此测试数据准备工作自然而然地就发展到了平台化阶段。我们把这种统一提供各类测试数据的Restful API服务称为“统一测试数据平台”。
最初统一测试数据平台就是服务化了数据准备函数的功能并且提供了GUI界面以方便用户使用除此以外并没有提供其他额外功能。如图1所示就是统一测试数据平台的UI界面。
<img src="https://static001.geekbang.org/resource/image/51/25/51f2943559843cb2a7b521723fd7a325.png" alt="" />
后来随着统一测试数据平台的广泛使用我们逐渐加入了更多的创新设计统一测试数据平台的架构也逐渐演变成了如图2所示的样子。
<img src="https://static001.geekbang.org/resource/image/7d/0d/7d4cdac895834f96e777234a0f6db40d.png" alt="" />
接下来,我和你分享一下统一测试数据平台的架构设计中最重要的两个部分:
<li>
引入了Core Service和一个内部数据库。其中内部数据库用于存放创建的测试数据的元数据Core Service在内部数据库的支持下提供数据质量和数量的管理机制。
</li>
<li>
<p>当一个测试数据被创建成功后为了使得下次再要创建同类型的测试数据时可以更高效Core Service会自动在后台创建一个Jenkins Job。这个Jenkins Job会再自动创建100条同类型的数据并将创建成功的数据的ID保存到内部数据库当下次再请求创建同类型数据时这个统一测试数据平台就可以直接从内部数据库返回已经事先创建的数据。<br />
在一定程度上这就相当于将原本的On-the-fly转变成了Out-of-box缩短整个测试用例的执行时间。当这个内部数据库中存放的100条数据被逐渐被使用导致总量低于20条时对应的Jenkins Job会自动把该类型的数据补足到100条。而这些操作对外都是透明的完全不需要我们进行额外的操作。</p>
</li>
这就是测试数据准备的3.0时代的最佳实践了。关于这个统一测试数据平台,如果你还想了解更多的技术细节,欢迎你给我留言,我们一起讨论。
## 总结
我和你分享了测试数据准备2.0时代的Builder Pattern实践以及3.0时代的统一测试数据平台。
2.0时代的Builder Pattern在提供了最大限度的数据灵活性的同时还保证了使用上的最大便利性并且维护成本还非常低。如果不考虑跨平台能力的话Builder Pattern已经是一个接近完美的解决方案了。
3.0时代统一测试数据平台其实是将所有的数据准备函数在Spring Boot的支持下转变为了Restful API为跨平台和跨语言的各类测试框架提供了统一的数据准备方案。
## 思考题
关于统一测试数据平台由于引入了Core Service和内部数据库所以可以在此基础上实现更多的高级功能。对此你觉得还可以引入哪些功能呢
感谢你的收听,欢迎你给我留言。