Blog | Phodal - A Growth Engineerhttp://www.phodal.com/blog/2020-03-23T11:25:41+00:00Blog如何用 DDD 结合 TDD 的思想『分治』复杂问题?2020-03-23T11:25:41+00:002020-03-23T09:26:25.775967+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/use-ddd-tdd-resolve-problem/PS:理论上,我应该在上个月 “交付” 这篇文章,自觉得有一些论据不够强有力。但是,因为疫情的原因,我离我的书架很远(电子书不方便翻阅)。所以回到杭州,搬完家后,我便继续补充这篇文章剩下的部分。
软件开发是一项复杂的集体活动,它涉及到一系列的行为和艺术,如项目管理、流程管控、知识转化、程序员心理学(狗头)等。从个体出发时,这些都是一些无关紧要的因素。作为一个 “螺丝钉”,我们所关心的是:如何去解决问题?当然了,我们把组织视为一个个体时,我们也只关心:如何去解决问题?
本文是我在写 Chapi 过程中的一个感悟与过程的思考,因为它具有一定的代表性,所以记录一下供大家参考。
TL;DR 版:没有,建议阅读全文。
## 问题的模式
### 通用问题的模式
所以,为了解决『如何解决问题』这个问题,我们开始尝试各种各样的解决方案,如 Cynefin 框架,还有 DDD(领域驱动设计),BDD(行为驱动开发),TDD(测试驱动开发)等等。我们所做的事情是寻找合理的原则与模式,以更好地解决问题,并记录这些实践,比如此文。
在一个团队、组织中,往往会强调文档和纪律的重要性 —— 你按文档上的过程来,不会出错(出错了,也不怪你)。因为前人总结了大量的经验,记录在案,告诉了你:遇到这样的问题,你应该这么做。顺便一提,这种做法各有利弊,不好的一个方面是:缺乏灵活性;缺少解决问题的能力。
回到,我们的元问题上,**如何解决『如何解决问题』这个问题?**
这个时候,我们可以尝试使用第一个模式,使用 Cynefin 框架。于是乎:
1. 简单的问题,寻找最佳实践。
2. 繁杂的问题,寻找最好的实践。
3. 复杂的问题,探索、尝试,如转换为繁杂问题。
4. 混沌,尝试采取行动,转向复杂问题。
5. 失序。
最后,我们的问题就落到了:如何将复杂问题拆解到人能处理的范围?换句话来说,作为一个资深的管理者,又著程序员、Tech Lead,我们要做的事情就是将复杂问题拆解,并交给合适的人解决。顺便一提,合适这一点很难,因为合格的领导者阶除了要解决问题,还要考虑人的成长。
### 软件开发中解决问题的模式
回到软件开发的问题上来,在这个一领域,我们往往面对的都是复杂问题。除非,你遇到的问题是,你有一个亿但是你不知道做点什么,你却想做点什么,这个看上去就像是一个混沌的问题。总而言之,我们要解决的都是复杂问题,于是我们可以寻找一些合适的现成模式:
- 使用 DDD,将复杂问题转化为繁杂问题
- 使用 TDD,将繁杂问题转化为简单问题
- 在简单问题中,使用最佳实践
嗯,听上去就是这么简单。
#### 将复杂问题转化繁杂问题:DDD
在软件开发这个行业里,人们已经总结了大量的模式和解决问题的手法。只是呢,因为你一直在加班 + 生活,可能没有时间去了解一些潜在的解决方案。于是,我们可以尝试的第一个方案就是使用 DDD(领域驱动设计)的方式来解决复杂的问题。
于是,我们开始了旅程:
1. 提炼问题域
2. 找到问题域中的第一个核心子域
3. 对第一个核心子域进行战略设计
4. 完善统一语言
5. 视情况重复 2 ~ 5
过程中,我们所做的是聚焦、聚焦、再聚焦,解决核心的高价值问题,再逐一突破。
#### 将繁杂问题转化简单问题:TDD
在我们分而击之之后,我们就回到一个小的问题上面,开始我们的编码。现在,我们就开始了我们的代码之旅。在不考虑一些技术细节的问题之后,我们的过程变成了:
1. 拆解问题(Tasking)
2. 红-绿-重构代码
3. 重构模型以加深理解
步骤,我们很熟悉,那么怎么拆解问题呢?这个问题,又变成了一个复杂问题,我们需要识别出大部分的场景,而后针对每一个场景编写独立的测试。换句话来说,我们要重新尝试合适的切入点,而后再逐一解决问题,然后我们会形成最佳实战。
## 聚焦:核心域
如果把软件开发想象成是一场军事活动,那么核心域相当于是敌方(问题)的主力。我们所要做的就是,选择一个合适的方式来击溃敌军,以少胜多是我们一直追求的,但是没有银弹。所以,我们需要把关注力放置在核心域上面。
### 不同核心域
组织架构往往是金字塔式的层级架构。尽管,我们已经达到了组织所认为的关键核心所在。但是事实上,在这一个系统中,每个人所关心的核心要素都是有所不同的:
- 组织统一核心域
- 技术部分核心域
- 代码中的核心域
- 个人的核心域
每个人对于统一目标的想法,会影响整个的行动。所以,组织中往往会强调个人兴趣与组织目标的一种对齐。这样一来,才能真正有效地在核心域实施行动。所以,最后就会落到人的问题上。毕竟,每个团队都需要一些强有力的个人才能保障。
### 适应核心域的变化
核心域并非一成不变的,它会随着业务中心的转移发生迁移。因为核心域会随着市场门槛的降低,慢慢变成通用域。
也因此,我们需要寻找新的核心域。而原先我们的支撑域,可能因为种种原因,转变为核心域。一个有意思的故事是牛仔裤的故事,原生它只是淘金者 Levi Strauss 的支撑域,核心域是淘金,随后它凑成变成了 Levi's 的核心域。不同公司、不同项目的核心域往往有可能相同的。
### 重新聚焦核心域
这一点,实现上是对于个人而言。尽管你是一个系统的核心部分,但是你做的事情,是不是依旧核心,这就是一个非常有意思的问题了。因为随着焦点的转移,你做的事情可能对于你来说价值不大。
## 拆解:驱动开发
我们已经有太多的关于驱动开发的模式:
- 测试驱动开发
- 行为驱动开发
- 验收驱动开发
它们以不同的视角来尝试拆解一个复杂的问题,从一个用户故事的验收开始,考虑一个个的用户场景,再落到某一个函数的实现。随后,通过这一个个的用例完善促成整个用户故事的完整性,以实现代码的价值。
### 分解任务,分离关注点
标题即内容。
### 从无序分解到有序实施
开始的时候,你并不是一帆风顺的。尝试一个新技术、框架的时候,我们总是一点点领悟出来的,所以我们会从一个大方向正确,但是小小的走弯,直到正确的抽象出特定的模式。以 Chapi 语法解析作为示例,尝试了不同的方案之后,最后出现了一个统一的步骤和模式,如:
1. 解析依赖
2. 解析 data struct
3. 解析 function name
4. ……
这样一来,当我们遇到不同的语言时,我们都可以尝试这样的 solution。不过解决方案并不是通用的,还会遇到一些特殊的情况。
## 模式:持续改进
没有什么设计可以一次设计到位。举个最简单的例子,在我毕业的这 6 年里,我搬过 5 次家,住过 10 年或者是 20 年前设计的老房子,我经常看到一些设计不足:
- 20 年前的房子,房间的插座不够,客厅大到离谱,房间很小。
- 10 年前的房子,房间的插座够了,但是它在每个房间有个电视,还有网线。而我们用路由器和电脑(含平板)。
- ……
- 1 年前设计的房子,可能考虑不到在家办公的需求,所以疫情期间要买各种设备。
因为种种原因,比如家有老小,所以你可能像我一个同事一样在厨房办公,它又大又安静。
所以啊,你设计的系统能满足未来一年的变化,已经非常了不起了。我们能所做的就是预留空间、引入创新文化,以便持续改进。
### 完善:领域模型重构
慢慢地,当我们接入越来越多的需求时,会发现模型会发现一些变化,所以我们需要一些重构才能支撑起新的业务场景。如在 Chapi 中,我们遇到的第一个挑战是,有的语言它是基于函数的,如 Go等,而有的语言是基于类的,如 Java。
所以,我们需要对模型进行重构以及设计改进。
### 模式:演进的统一语言
如上。
## 回顾:缺失的地方
回顾,设计中缺失的地方,以便下次吸取一些经典(当然了,不要过度地预先设计):
- 模型的适用性。在设计的过程中,我假定了不同的编程语言使用的是同一个模型,但是模型缺少边界。
- 如何从代码中显现概念?毕竟代码上可能只有一个字段,一行注释。我所应对的一种方式是测试、查看调用方,还有知识共享的方式。
好了,这篇文章又写得太长了。
## 结论
事实上,你并不需要上述的驱动开发方式,你所需要的是:如何有序地解决一系列的复杂问题?
同时,你所归纳出来的这个方法,可以被快速地大规模复制。一个启发的文章是《[驱动方法不能改变任何事情](https://www.infoq.cn/article/star-driven-approaches)》,如文章所说,你需要创造出吸引人的基因(朗朗上口):
| 框架 | 它的承诺 | 吸引人的文化基因 |
|-----|---------|---------------|
| TDD | 你的产品将几乎没有可见的 bug,同时除了必须的代码外,不会生产过多的代码。 | 红 - 绿 - 重构,单元测试 |
| BDD | 你将 TDD 与功能需求关联起来 | Given, When, Then |
| DDD | 应用的架构完全反映现实业务,因此后续需求的实现将非常自然;没有变通方法,没有秘密路径,只有纯粹的、不受影响的以及独立的模型。 | 组成部分(Building Blocks),无歧义的语言(Unambiguous Language)和策略设计(Strategic Design) |
顺带吐槽一句,从这个基因来看,DDD 有点复杂。
如果没有的话,那么使用别人总结的方法,是一种更省事的方式。【架构拾集】移动应用的自动化测试(BDD 方式)2018-10-14T12:09:59+00:002018-10-14T12:10:26.784442+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/phodal-architecture-101-mobile-appllication-test-architecture/我的上一篇关于自动化测试的文章,大抵已经在一年以前——一篇关于前端自动化测试的 BDD 框架对比。这么长的时间里,没有相关的文章,总得给自己找一个合适的理由。
编写测试是开发人员日常工作的一小部分,但并非是全部。即使是专业的测试人员,自动化测试也并非是全部的工作。与此同时呢,有了单元测试之外,对于自动化测试的需求,并不是那么强烈。速度,是我们不考虑自动化测试的主要原因,运行起来慢了,进一步地导致编写测试也慢了。UI 上一旦有一丁点的修改,那怕是得引起多少的不愉快,不得不噗呲噗呲地去修改测试。我想说的无非就是,**避免编写 “可怕” 地自动化测试**,尽量用你的单元测试来保障质量。要是你们有 KPI 的限制,请将以上的文字当成废话。
似乎在有些公司里,自动化测试、单元测试并不是技术负责人要考虑的问题,可在我司并非如此,测试也在技术的范围里。每每开始一个项目时,就不得不去考虑自动化测试的问题,选用什么框架合适、需要前后端如何配合、怎样去替换第三方的服务。这些内容完全交给测试人员吧,怕是会遇到一些不顺。测试人员有自己的测试技术栈,拥有自己的 “银弹” —— 过去的经验和代码库。哪怕经验再丰富的测试人员,有时遇到一些新的项目、技术栈,这些东西可能就用不上了,又或者是使用某些框架可能会更加便利。因此呢,要成为更好的技术人,测试也是要考虑的范畴。
说到 Web 方面的自动化测试,我算是个有经验的老手。从顶层的 DSL 到底层的 Web Driver,到底来说,还是颇有经验的。可是说到 APP 的自动化测试,在项目上尝试过,但也不敢说经验丰富。而最近的项目,正在实施相应的移动应用自动化测试。看了看方案,与之前的 Web 或者是 React Native 的自动化测试,从底层对比架构吧,相似、差距也不是太大。便想着写篇文章来记录一下,相应的架构设计。
## 技术远景
作为一个团队的技术负责人,我希望:拥有一个移动应用测试架构,它能快速让测试人员快速上手——阅读、编写测试用例。与此同时,我希望这些测试用例是能让非技术人员阅读,诸如业务分析人员,并且符合真实的用户使用场景。
## 架构设计
当我们谈到业务分析人员也能编写的测试,我们说的只有 BDD(Behavior Driven Development,行为驱动开发)。它是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA(测试人员)和非技术人员或商业参与者之间的协作。
BDD 在这一种上相当的迷人——能让非技术人员编写测试。而其核心的部分在于:创建了一个环境隔离的 DSL,仿人类语言的 DSL。咳咳,这个 DSL 实现起来,可不轻松。关注顶层 DSL 的同时,开发人员还要努力实现好底层的实现。举个简单的例子,如下是之前在 BDD 一文中的 DSL 示例,这是顶层的设计:
```markdown
功能: 失败的登录
场景大纲: 失败的登录
假设 当我在网站的首页
```
对应的,开发人员需要编写实现:
```javascript
...
Given('当我在网站的首页', function() {
return this.driver.get('http://0.0.0.0:7272/');
});
..
```
从上述的代码中,一眼就可以看出复杂的地方,实现一个领域特定(业务特定)的 DSL 语言。
我们要完成的 DSL 实现,上层是提供一个 DSL,下层则是对接 driver 的 Agent 层。在 Web 领域里,这个 driver 的 Agent 层负责对接不同的浏览器,诸如 Selenium,driver 则视不同的浏览器而有所不同,如 ChromeDriver、FirefoxDriver、PhantomJSDriver 等等。Selenium 这样的测试框架,除了通过 driver 直接操作了浏览器,还提供了不同语言的编程接口。
相似的,在 APP 领域也有这样的方案,它要通常这个 agent 来连接物理设备,并提供一系列的编程接口。
## 架构设计方案
对整个架构有了一个基本的认识之后,让我们继续往下移动,来重新发掘一下:我们究竟需要哪些基本元素?
- BDD 测试框架,为开发人员提供可创建 DSL 的接口。
- 移动设备的测试编程接口,提供一个操作移动应用的接口。
- 连接移动设备的操作库,即移动端的 WebDriver。
- 用于编写测试时的 UI 检查工具。
从这一点上来看,它与 Web 应用的 BDD 架构差不多。为此,我们需要准备如下的一些框架:
- Robot Framework,一个支持 BDD 的、基于 Python 编写的功能自动化测试软件框架。
- Appium,是一个开源测试自动化框架,用于原生,混合和移动 Web 应用程序。它使用 WebDriver 协议来驱动 iOS、Android 和 Windows 应用程序。
- XCUITest Driver,基于 Apple 官方的界面自动化测试 XCUITest 封装的测试接口,可以直接执行 iOS 的自动化测试。
- UiAutomator2 Driver,则是 Google 官方提供的用于 Android 系统的测试接口,可以直接执行 Android 的自动化测试。
- Appium Inspector,用于查找 iOS/Android 上的元素
- UiAutomator Viewer,由 Android SDK 自带的元素查找工具。
由于我们计划的顶层是由 DSL 来实现,而对应的 BDD 层实现是由 Robot Framework 来完成的。Robot Framework 使用的是 Python 语言,我们就需要找到对应的 Python 主要依赖有:
- robotframework,即 Robot Framework 本身
- robotframework-appiumlibrary,用于为 Robot Framework 提供 Appium 相应的接口封装
- robotframework-ride,用于 Robot Framework 的测试数据编辑器
有了这些主要的库,我们就可以编写我们的 DSL?不,我们还需要配置好,对应的移动端 Driver。
**Android Driver 依赖**
比较简单,通过 ``appium-uiautomator2-driver`` 库就拥有了 driver。
**iOS Driver 依赖**
为了实现对 iOS 设备的自动化测试,需要安装 XCUITest,为此我们需要下面的这一系列工具:
- ``libimobiledevice``,是一个跨平台的用于与 iOS 设备通讯的协议库,它可以让 Windows、macOS、GNU/Linux 系统连接 iPhone/iPod Touch 等iOS 设备。
- ``carthage`` 是一个简单的、去中心化的依赖管理工具。
- ``ios-deploy`` 是一个使用命令行安装 iOS 应用程序到连接设备的工具
- ``xcodebuild``,是苹果发布自动构建的工具。
- ``xcpretty``,用于对 xcodebuild 的输出进行格式化,包含输出 report 功能。
看,有了这一系列的知识,我们几乎知道怎么做搭建移动应用的自动化测试。
## 结论
还是 Web 大法好。BDD 框架对比: Cucumber.js vs Robot Framework vs Gauge.js2017-10-19T13:45:25+00:002017-10-19T13:46:44.588876+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/how-to-choice-you-bdd-framework-robot-cucumber-gauge/
最近,项目上出于系统性稳定性、减少测试工作量考虑,打算在 Web 前端引入 BDD。由于上一个项目写了一定的 Cucumber 代码(BDD 测试框架之一),这个框架选型的责任便落到了我的肩膀上了。
在我们进行框架选型的时候,着重考虑了一个因素:**测试实现脚本是由开发人员编写的,因此最好寻找 JavaScript 支持的框架**。在搜索了一天后,选择了三个框架
Cucumber、Robot、Gauge。以下是上述的三个框架入选的原因:
**Cucumber**,团队的开发人员有一些有相关的开发经验、支持 JavaScript。
**Robot Framework**,测试人员接受过相关的培训、不支持 JavaScript。
**Gauge**,可以生成更好的测试报告及自由的书写、支持 JavaScript。不过,主要是我写腻了
Cucumber。
随后,便使用三个不同的框架写了几个 UI 测试的
DEMO。在开始之前,让我们了解什么是 BDD。
BDD
---
> Behavior Driven Development,行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA 和非技术人员或商业参与者之间的协作。
与一般的自动化测试(如单元测试、服务测试、UI 测试)不一样的是,BDD
是由多方参与的测试开发方式。如在使用 Protractor 写 Angular 的 E2E
测试的时候,所以的测试都是前端测试人员编写的。BDD 最重要的一个特性是:由**非开发人员编写测试用例**,而这些测试用例是使用**自然语言编写的 DSL(领域特定语言)**。
换多话来说,业务人员、测试人员、客户等利益相关者,以习惯的方式编写相关的测试用例,再由开发人员去实现相关的测试。如下图所示:
![BDD 流程](https://phodal.github.io/bdd-frameworks-compare/docs/bdd_process.jpg)
由业务人员编写的测试用例,将是使用如下的形式实现的:
```
* 当我在网站的首页
* 输入用户名 "demo"
* 输入密码 "mode"
* 提交登录信息
* 用户应该跳转到欢迎页
```
对于能支持中文的 BDD
框架来说,这就是业务人员和测试人员等编写的用例,他们能轻松地编写出这样的用例,而开发人员便是去实现这一个又一个的
DSL 语句。
在我之前的一个项目里,我们遇到了一个问题:**测试用例也是由开发人员编写的**。这种做法不仅不能体现 BDD 的价值,而且对于开发人员来说,这是在糟蹋代码。如果完全是由开发人员编写的测试,那么为什么我们需要写一个额外的 DSL 层呢?
接下来,让我们看看三个测试的一个简单对比表:
BDD 框架对比: Cucumber.js vs Robot Framework vs Gauge.js
---
x | Cucumber |Gauge | Robot
------------|------------------------------------|----------------------|-----------
编程语言支持 | Java,Ruby,JavaScript 等 13 种语言 | Java, JavaScript, Ruby 等 6 种语言 | Python, Java, C
支持的系统 | 所有主流系统 | 所有主流系统 | 所有主流系统
多语言支持 | UTF-8 | UTF-8 | 用户关键字及用例层面支持UTF-8
中文社区支持 | 完善 | 待完善 | 完善
Report | JS 不支持 HTML | 粗粒度 | 细粒度
失败时截图 | 不支持 | 支持 | 支持
从某程程度上来看,三个框架差不了多少,每个框架也各自都有自己的问题。
1. Cucumber 的 Javascript 版本不支持 HTML 的报表生成。
2. Gauge 虽然比较适合我们的要求,但是相关的中文资料比较少。
3. Robot 主要的问题是不支持 JavaScript,以及要按 Robot
定义的方式来编写代码。
以下是三个框架的示例及详细的对比。
Cucumber.js
--
> Cucumber 是一个能够理解用普通语言 描述的测试用例的支持行为驱动开发(BDD)的自动化测试工具,用Ruby编写,支持Java和.Net等多种开发语言。
- 使用自然语言,更易读
- 支持表格参数
- 支持多种格式的Report:html、junit etc.
- 支持多种语言
- 支持四种状态的测试步骤:Passed、Failed、Skipped、Pending
- 支持使用变形器消除重复
- 一个商用的在线 Cucumber 系统:Cucumber Pro
### DSL Code Examples
示例代码:[https://github.com/phodal/bdd-frameworks-compare/tree/master/cucumber](https://github.com/phodal/bdd-frameworks-compare/tree/master/cucumber)
```cucumber
# language: zh-CN
功能: 失败的登录
场景大纲: 失败的登录
假设 当我在网站的首页
当 输入用户名 <用户名>
当 输入密码 <密码>
当 提交登录信息
那么 页面应该返回 "Error Page"
例子:
|用户名 |密码 |
|'Jan1' |'password'|
|'Jan2' |'password'|
```
Cucumber 支持比较固定的 DSL 格式,即三段式
Given-When-Then,对应的中文便是:假设-当-那么。作为一个历史悠久的框架,它的中文资料相当的丰富,只是在
JavaScript 方面有些不足,不能生成对应的 HTML 报告。
其实现代码如下所示:
### Step Code Examples
```javascript
defineSupportCode(function({Given, When, Then}) {
Given('当我在网站的首页', function() {
return this.driver.get('http://0.0.0.0:7272/');
});
When('输入用户名 {string}', function (text) {
return this.driver.findElement(By.id('username_field')).sendKeys(text)
});
When('输入密码 {string}', function (text) {
return this.driver.findElement(By.id('password_field')).sendKeys(text)
});
When('提交登录信息', function () {
return this.driver.findElement(By.id('login_button')).click()
});
Then('页面应该返回 {string}', function (string) {
this.driver.getTitle().then(function(title) {
expect(title).to.equal(string);
});
});
});
```
从代码实现上来说,也是固定的三段式。其底层依赖于 Selenium,因此写法上与
Gauge 的区别并不大。
Robot Framework
---
> Robot Framework是一款python编写的功能自动化测试框架。具备良好的可扩展性,支持关键字驱动,可以同时测试多种类型的客户端或者接口,可以进行分布式测试执行。
关键特性:
- 使用关键字的机制,更容易上手
- 提供了RIDE,对于不熟悉编码的人来说比较友好
- 能够精细的控制关键字的scope
- Log 和 Report 非常好
- 使用变量文件的机制来描述不同的环境
- 丰富的关键字库
- 内置变量
### DSL Code Examples
示例代码:[https://github.com/phodal/bdd-frameworks-compare/tree/master/robot](https://github.com/phodal/bdd-frameworks-compare/tree/master/robot)
```robot
*** Settings ***
Documentation 登录测试 2
...
Suite Setup 打开浏览器到登录页1
Suite Teardown Close Browser
Test Setup 转到登录页
Test Template 使用错误的失败凭据应该登录失败
Resource resource.robot
*** Test Cases *** USER NAME PASSWORD
无效的用户名 invalid ${VALID PASSWORD}
无效的密码 ${VALID USER} invalid
无效的用户名和密码 invalid whatever
*** Keywords ***
使用错误的失败凭据应该登录失败
[Arguments] ${username} ${password}
输入用户名 ${username}
输入密码 ${password}
提交登录信息
登录应该不成功
登录应该不成功
Location Should Be ${ERROR URL}
Title Should Be Error Page
```
从上面的代码来看,Robot
在某些特定的关键字上,必须使用英语。在关键的代码如关闭浏览器,仍然需要使用
Close Browser 英语这些来实现。
### Step Code Examples
```robot
打开浏览器到登录页
Open Browser ${LOGIN URL} ${BROWSER}
Maximize Browser Window
Set Selenium Speed ${DELAY}
Login Page Should Be Open
Login Page Should Be Open
Title Should Be Login Page
转到登录页
Go To ${LOGIN URL}
Login Page Should Be Open
输入用户名
[Arguments] ${username}
Input Text username_field ${username}
输入密码
[Arguments] ${password}
Input Text password_field ${password}
提交登录信息
Click Button login_button
应该跳转到欢迎页
Location Should Be ${WELCOME URL}
Title Should Be Welcome Page
```
与上面的 Cucumber 相比,Robot
对于英语的非开发人员来说更加友好。换句话来说,Robot 更像是一个适合于 QA
的语言。作为一个开发人员,可能不太喜欢这种形式。
### 报告示例
不过,Robot
提供了一份说尽的报告。细致的展示了每一个测试,以及其步骤时间等等。
![Robot Framework Report](https://phodal.github.io/bdd-frameworks-compare/docs/docs/robot-report.png)
Gauge
---
> Gauge 是 Go 开发的一个跨平台测试自动化工具。它给作者提供了用商业语言测试用例的能力。
关键特性:
- 基于 markdown 的丰富的标记
- 支持用任何程序语言来编写测试代码
- 支持 plugin 的模块化架构
- 跨语言实现一致性。
- 简单,灵活和丰富的语法
- 开源的,因此它可以自由共享,同时被他人改进
- 商业语言测试 : 支持可执行文件的概念
- 帮助您创建可维护和可理解的测试套件
- 支持外部数据来源
- IDE Support
### DSL Code Examples
示例代码:[https://github.com/phodal/bdd-frameworks-compare/tree/master/gaugejs](https://github.com/phodal/bdd-frameworks-compare/tree/master/gaugejs)
```markdown
失败的登录
===
|用户名 |密码 |
|--------|--------|
|Jan1 |password|
|Jan2 |password|
失败的登录
-----------
* 当我在网站的首页
* 输入用户名 <用户名>
* 输入密码 <密码>
* 提交登录信息
* 页面应该返回 "Error Page"
```
与 Robot 和 Cucumber 不一样的是,Gauge 使用的是大家更熟悉的 Markdown
形式的 DSL。并且从形式上来说,更加自由。List
中的每一行,就代表着一个元素。因此,其对应的实现代码也更加的自由。
### Step Code Examples
```javascript
step("当我在网站的首页", async function () {
await page.goto('http://0.0.0.0:7272/');
});
step("输入用户名 <query>", async function (query) {
await page.click('#username_field');
await page.type(query)
});
step("输入密码 <query>", async function (query) {
await page.click('#password_field');
await page.type(query)
});
step("提交登录信息", async function () {
await page.click('#login_button')
});
step("页面应该返回 <query>", async function(query){
await page.waitFor('h1');
const text = await await page.$eval('#container h1', h1 => {
return h1.innerHTML;
});
expect(text).to.equal(query);
});
```
上面采用的是 Node.js 8 支持的异步写法,除此与 cucumber.js
写的代码并没有太多的差异。
### 报告示例
至于,Gauge 生成的 UI 并没有 Robot 那么详细,但是看上去现代。
![Gauge Report](https://phodal.github.io/bdd-frameworks-compare/docs/gauge-report.png)
</query></query></query>用Jasmine和Sinon测试Backbone应用 Part 1[译]2014-11-06T14:11:22+00:002014-11-06T18:03:51.870337+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/use-jasmine-and-sinon-testing-backbone-applications/最近在慢慢深入Backbone,也试着写一些测试,找一些合适的文档来学习。于是就找到了一个系列的文章 : [Testing Backbone applications with Jasmine and Sinon – Part 1](http://tinnedfruit.com/2011/03/03/testing-backbone-apps-with-jasmine-sinon.html)
##概览
这是第一次展示如何测试Backbone.js应用的一系列文章,在这里我们使用Jasmine BDD测试框架以及Sinon.JS库的spying,stubbingt和mocking。
在这一部分,我们将简单地说一下Backbone,紧接着我们将介绍Jasmine和Sinon的一些细节。在这个过程中,我们将会看到这些工具是如何完美地测试Backbone应用。
##无处不在的Backbone
最近的几个月里,随着大量教程的推出以及一些高性能的实现,使得Backbone获得了更多的关注。
我们可以用下面的原因来解释Backbone的流行。它提供了一个最小化的model-view-controller(模式-视图-控制器)的框架来帮助开发者控制复杂的代码,但是与此同时它可以与其他一些框架一起工作。这就意味着,与Cappuccino这些富JS UI框架相比,Backbone不提供UI框架或者主题,但是它可以让开发者去选择DOM库。Backbone对于jQuery或者Zepto支持得特别好,但是你也可以使用别的库来代替他们。
Backbone MVC框架本身可以很好地支持自下而上的单元测试。models,collections,views和routers的隔离,意味着每个部分的行为可以单独地测试,使调试更简单。
对于那些有MVC框架(如Rails和Django)开发测试经验的人来说,这部分的开发也会很熟悉。很有多成熟的单元测试库,工具和方法可以测试这些应用。你只需要去写Javascript测试去保证你的前端代码和你的服务端代码同样的高效。
##Jasmine BDD
(这里就不引用原文对于Jasmine的介绍了,简单地来说就是下面这样子)
- 不依赖于其它任何框架;
- 不需要DOM支持(也就是说可以脱离浏览器!);
- 行为驱动式语法,写起来非常简单;
- 支持对异步调用的测试;
- 可以在多种环境中运行(包括Ruby、NodeJS、Scala、Java、BPM、.net、Perl)
###Specs
多说无益,让我们来看看一个简单地用于Backbone Model的Jasmine测试。
it("should expose an attribute", function() {
var episode = new Backbone.Model({
title: "Hollywood - Part 2"
});
expect(episode.get("title"))
.toEqual("Hollywood - Part 2");
});
一个spec只是简单地描述了一下预期的行为,上面的代码会执行该行为,以及一种或多种期望测试行为的代码。
个别spec应短而且只测试行为的一个方面。如果你发现自己写了一些不同的期望或spec变得非常长,那么考虑将它变成其他spec。将你的spec与suites分组,并使用set up和teardown函数可以帮助这一点。
###Suites
Specs分组为Suites,这被定义在describe()函数。例如,所有的specs为集模式可以被分成一组,如下所示:
describe("Episode model", function() {
it("should expose an attribute", function() {
...
});
it("should validate on save", function() {
...
});
});
很不错的一点是,Suites也可以嵌套。当你有很多specs的,因为你可以将它们组织成组离散块。我喜欢用一个描述块包与特定语境出发specs。这更好地保留了specs的会话风格。例如:
describe("Episode model", function() {
describe("when creating a new episode", function() {
it("should expose the title attribute", function() {
...
});
it("should have a default parental rating", function() {
...
});
});
});
###beforeEach() and afterEach()
作为传统风格的xUnit测试框架,可以选择指定的代码在每次测试后运行。这是一个很不错的功能,以确保一致的条件下对每个试验,并用于设置变量和对象中的规格使用。
下面的示例使用beforeEach()来创建一个用于每个sepc的Model实例。
describe("Episode", function() {
beforeEach(function() {
this.episode = new Backbone.Model({
title: "Hollywood - Part 2"
});
});
it("should expose an attribute", function() {
expect(this.episode.get("title"))
.toEqual("Hollywood - Part 2");
});
it("should validate on save", function() {
...
});
});
你可以提供一个beforeEach()和afterEach()方法在每个嵌套的describe,接着在你的specs,让你拥有专为每个Suites的规格一般或者特殊setup()和teardown()。正如您将在本文的其它部分看,这是非常方便的确实减少重复和控制每个specs的确切条件。
###The spec runner
这种结构使得specs对于其他开发者能够直易读和容易理解的。,主要是因为对每个specs的说明以及期望的匹配的格式。
Jasmine还提供了一个简单的spec runner,这简直是与将运行所有你所提供的规范的脚本的HTML页面。下面显示了一个suite的specs与单一spec故障的输出:
![the spec runner][1]
我们将推出茉莉花其他一些有用的功能在本文的其它部分,因为我们需要他们,包括创建固定装置,使用jQuery和创建自己的自定义期望的匹配。现在,到Sinon.JS。
我们将在本文的其他部分说明一些Jasmine有用的功能,因为我们需要这些功能,包括creating fixtures,使用jQuery和创建自己的自定义期望的匹配。现在,让我们试试Sinon.JS。
##Sinon.JS
Sinon.js提供了fake对象——spies,stubs和mocks,是一个专用于Javascript的个测试辅助工具。利用这些结构在测试JavaScript代码是不是已经真正流行起来,只是刚刚开始。但是,如果你正在开发一个丰富的,复杂的应用程序,如您在使用Backbone,然后fake对象是测试工具集的一个非常有用的部分。
Christian Johansen——Sinon.JS的创建者,解释了为什么你需要fake。在Javascript中,这些原因可以分为
- 性能 真实的DOM操作,依靠定时行为及网络活动减慢了测试
- 隔离 单元测试应把重点放在小的一块功能成为可能,并解耦不可靠的或低依赖
使用fake对象是拥抱TDD和BDD的基本组成部分。他们基本上是让代码能够脱离其依赖进行测试。任何API或模块测试你的代码依赖于可以fake的,你需要为你的测试的方式作出回应。您也可以检查fake的方法,看看测试的过程中,究竟他们是如何被调用。
Sinon.JS允许你提供fake几乎所有的东西。您可以将自己的应用程序的fake,在jQuery的,当XMLHttpRequest API本身特定的行为,或者你甚至可以fake JavaScript的计时方法,对于有时间依赖性,如动画和超时快速的测试代码。
Sinon.JS提供了三种类型的fake对象:spies, stubs and mocks。
###Spies
(ps:这部分翻译得很烂)
Spies是跟踪的方式和他们被调用的函数,以及返回想要的值。这是异步的,这在事件驱动的应用非常有用,你可以发送一个spy以记录正在发生的事情,即使这些方法都是匿名或封闭。
spies可以“匿名”,也可以对现有功能的spy。
一个匿名的spy只是用spying功能的空函数,可以记录下它是如何被使用的。像一个真正的speis 被送往敌后与连接到它的胸口的麦克风,而测试方法是不知道。这里是一个spy测试一个简单的Backbone自定义事件绑定的例子:
it("should fire a callback when 'foo' is triggered", function() {
// Create an anonymous spy
var spy = sinon.spy();
// Create a new Backbone 'Episode' model
var episode = new Episode({
title: "Hollywood - Part 2"
});
// Call the anonymous spy method when 'foo' is triggered
episode.bind('foo', spy);
// Trigger the foo event
episode.trigger('foo');
// Expect that the spy was called at least once
expect(spy.called).toBeTruthy();
});
这测试将会通过,当spies被调用一次或多次,不管它是如何调用或什么参数。然而,Sinon提供了一些方法,让你严格的限制调用次数,的确是每次调用看起来都像,spy返回了什么。
Spying行为也可以被连接到一个现有的方法。欢快的,我喜欢叫这些“moles”。这是有用的,以检查某些小部分的功能被调用的代码的另一部分与预期一样。例如,您可能要检查模型的保存方法,确保正确的jQuery $.ajax 调用 :
it("should make the correct server request", function() {
var episode = new Backbone.Model({
title: "Hollywood - Part 2",
url: "/episodes/1"
});
// Spy on jQuery's ajax method
var spy = sinon.spy(jQuery, 'ajax');
// Save the model
episode.save();
// Spy was called
expect(spy).toHaveBeenCalled();
// Check url property of first argument
expect(spy.getCall(0).args[0].url)
.toEqual("/episodes/1");
// Restore jQuery.ajax to normal
jQuery.ajax.restore();
});
###Stubs and Mocks
保留原文关于Stubs和Mocks的解释
Stubs and mocks in Sinon implement all the features of spies, but with some added features. Stubs allow you to replace the existing behaviour of a particular method with whatever you like. This is great for emulating exceptions and error scenarios from external dependencies so you can test that your code will respond appropriately. It also allows you to start development when other dependencies are not yet in place.
Mocks provide all this, but instead mock an entire API and set built-in expectations on how they will be utilised. Like spies they track how they have been used, and like stubs they respond in a pre-programmed manner according to the needs of the test. However, unlike a spy, the expectations for their behaviour is pre-programmed, and a single verification step at the end will fail if any of these individual expectations are not met.
简单的来说
Stubs就是返回你想要的结果。
Mocks就是确保方法被调用 。
- Stub是代码的一部分。其目的就是用简单的行为替换复杂的行为,从而允许独立地测试代码的一部分。
- Mock Object是使用来代替与你的代码协作的对象的对象,这样代码可以调用Mock Object的方法,这些方法的调用的结果是由你的测试设置好的。
相关文章: [mock stub与tdd](http://www.phodal.com/blog/mock-vs-stub-in-tdd/)
###Fake Ajax and fake servers
Sinon不局限于Spying on和stubbing普通函数和方法。它还提供了快捷的伪造Ajax响应方式。这意味着您可以从您的JSON数据源完全隔离测试你的代码,并且不依赖于以运行spec suites运行的Web应用程序。此外,你可以测试你的应用程序响应适当的时候将其从幸福的路径,包括无效的JSON和各种HTTP响应代码偏离。
这里有用于Backbone模型spec的获取方法,它使用一个fake的server来响应Ajax请求的一个简单的例子:
describe("Episode model", function() {
beforeEach(function() {
this.server = sinon.fakeServer.create();
});
afterEach(function() {
this.server.restore();
});
it("should fire the change event", function() {
var callback = sinon.spy();
// Set how the fake server will respond
// This reads: a GET request for /episode/123
// will return a 200 response of type
// application/json with the given JSON response body
this.server.respondWith("GET", "/episode/123",
[200, {"Content-Type": "application/json"},
'{"id":123,"title":"Hollywood - Part 2"}']);
var episode = new Episode({id: 123});
// Bind to the change event on the model
episode.bind('change', callback);
// makes an ajax request to the server
episode.fetch();
// Fake server responds to the request
this.server.respond();
// Expect that the spy was called with the new model
expect(callback.called).toBeTruthy();
expect(callback.getCall(0).args[0].attributes)
.toEqual({
id: 123,
title: "Hollywood - Part 2"
});
});
});
这个spec测试可以通过下面这个简单的Backbone Model
var Episode = Backbone.Model.extend({
url: function() {
return "/episode/" + this.id;
}
});
还有更多的兴农,我们这里不讨论。特别是,fake timer是为测试时间依赖的功能非常有用,例如动画,而不会减慢您的测试。
##总结
(ps:总结,就是翻译得一般般)
在Backbone应用的带血的世界里,复杂的异步和相互依存的行为可能会导致任何开发人员伤透脑筋。Backbone可以帮助开发人员构建自己的代码转换成小的,独立的models,collections, views和routers。但是,这是真的只是成功的一半。如果没有经过充分测试的代码将会有更多的未被发现的缺陷,而那些被发现将很难追查。其他团队成员可能会无意中破坏你的代码,或者干脆误解了它的目的。
[1]: /static/media/uploads/jasmine-spec-runner.png写作驱动学习2014-05-16T00:52:50+00:002014-11-10T06:27:28.925898+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/write-driven-learning/#写作驱动学习
在软件开发领域有这样的几个名词
- TDD。测试驱动开发(Test-driven development)是极限编程中倡导的程序开发方法,以其倡导先写测试程序,然后编码实现其功能得名
- BDD。行为驱动开发(Behavior-Driven Development)是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。
- DDD。领域驱动设计(Domain-Driven Design)的一个核心的原则是使用一种基于模型的语言。
而最近我处在于一个尴尬的位置:``写作驱动学习``(``Write-Driven Learing``)。开始之前我找到了一个相似的概念,``数据驱动学习``(Data-driven Learning)。
#起因
当我来到了一家卓越的软件公司时,开始有了点所谓的``骗子综合症(imposter syndrome)``(即常常会发现所有一起共事的程序员都比自己聪明、比自己有天份、比自己有才能),幸运的是并没有持续太多的时间。只是已经有了所谓的``21天效应``(在行为心理学中,人们把一个人的新习惯或理念的形成并得以巩固至少需要21天的现象,称之为21天效应。这是说,一个人的动作、或想法,如果重复21天就会变成一个习惯性的动作或想法。),在刚开始的时候里不断地学习,以减少这种所谓的``综合症``。然而,在这时已经养成了写作的习惯。
在过去的六个月里:
<table>
<thead>
<tr>
<th>时间</th>
<th>写作数量</th>
</tr>
</thead>
<tbody><tr>
<td>2013.12</td>
<td>27</td>
</tr>
<tr>
<td>2014.01</td>
<td>24</td>
</tr>
<tr>
<td>2014.02</td>
<td>12</td>
</tr>
<tr>
<td>2014.03</td>
<td>29</td>
</tr>
<tr>
<td>2014.04</td>
<td>49</td>
</tr>
<tr>
<td>2014.05</td>
<td>25</td>
</tr>
</tbody></table>
在我在学习更多东西的时候,我玩了更多的东西。而在这时候因为网站流量的关系,我开始写作记录更多的东西。而在那之前的半年里,也就是2013.06~2013.11的文章数加起来才21.
#结果
于是在其他程序员开始写程序的时候我开始写作,我得好好想想今天要什么,正如那些测试人员要开始写测试那样。于是,就有了``写作驱动学习``(Write-Driven Learning)。
##写作驱动学习的优点
<div class="posts">
- 对于我们所要学习的东西,我们可以掌握得更深入,因为我们需要去给别人解释。
- 除去收获所学习的东西,我们还可以有副产品——博客。
- 我们在以另外一种方式思问题。
- 我们在不断地对自己知识总结,对自己知识的一个强化。
- 当然了这是我们的笔记。
</div>
##写作驱动学习的缺点
<div class="posts">
- 可能会花费更多的时间,在诸如写作这点事情上。
- 有时学习的结果是为了写作。
- 付出不一定有相应的回报。
</div>
#后记
做为一个独立博客的作者来说我开始在不断地坚持写作,而唯一的动力源是来自于因为有更多的访客。要坚持写博客可能不是一件容易的事,但是现在似乎做到了,转变成一种``写作驱动学习``。
##写博客的动力
对于我来说写博客的动力应该是下面这些:
<div class="posts">
- 对于SEO的学习。
- 成为Full Stack的必经之路。
- 更好地去理解WEB经营。
- 提高自己的综合能力
- 分享知识与经验。
- 可以认识更多的人。
- 可以被发掘。
</div>cucumber——有趣的ruby bdd框架2014-01-02T21:33:53+00:002014-05-14T09:09:11.900601+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cucumber-a-funny-bdd-framework/这里只是因为看到当前的项目中有这个神奇的东西,所以试着去写点东西来记录这个过程,结果是有趣的。相比之下,更像是一种有趣的自然语言,看起来有种文档的感觉。放一下官方示例的一个结果,感觉很容易阅读,似乎还有中文,不过我想是用不到了。
[WARNING] MultiJson is using the default adapter (ok_json).We recommend loading a different JSON library to improve performance.
Feature: Hello World Feature
In order to ensure that my installation works
As a Developer
I want to run a quick Cucumber test
Scenario: Hello World Scenario # features/basic.feature:6
Given The Action is Hello # features/setp_definitions/step_steps.rb:3
When The Subject is World # features/setp_definitions/step_steps.rb:7
Then The Greeting is Hello, World # features/setp_definitions/step_steps.rb:11
1 scenario (1 passed)
3 steps (3 passed)
0m0.002s
==看上去是不是有点奇怪,这个就是结果。
##cucumber##
<blockquote>Cucumber 是一个能够理解用普通语言 描述的测试用例的支持行为驱动开发(BDD)的自动化测试工具,用Ruby编写,支持Java和.Net等多种开发语言。</blockquote>
如官网所说,Making BDD fun,确实有点意思。
官网给出的七个步骤
- Describe behaviour in plain text
- Write a step definition in Ruby
- Run and watch it fail
- Write code to make the step pass
- Run again and see the step pass
- Repeat 2-5 until green like a cuke
- Repeat 1-6 until the money runs out
##使用cucumber##
那么,就我们开始这个有趣的过程吧。
###安装###
gem install cucumber
省去一些废话,这里就只用这些来表达。
###使用cucumber###
最后项目结果如下所示
└── features
├── basic.feature
└── setp_definitions
└── step_steps.rb
下面是basic.feature的最后结果:
Feature: Hello World Feature
In order to ensure that my installation works
As a Developer
I want to run a quick Cucumber test
Scenario: Hello World Scenario
Given The Action is Hello
When The Subject is World
Then The Greeting is Hello, World
看上去和我们的运行结果有点相似,我们看看step_steps.rb
require 'rspec/expectations'
Given /The Action is ([A-z]*)/ do |action|
@action = action
end
When /The Subject is ([A-z]*)/ do |subject|
@subject = subject
end
Then /The Greeting is (.*)/ do |greeting|
greeting.should == "#{@action}, #{@subject}"
end
似乎这个看不出什么结果,等等,我们修改一下basic.features。
于是我们很愉快的将,Hello改为Hello1
[WARNING] MultiJson is using the default adapter (ok_json).We recommend loading a different JSON library to improve performance.
Feature: Hello World Feature
In order to ensure that my installation works
As a Developer
I want to run a quick Cucumber test
Scenario: Hello World Scenario # features/basic.feature:6
Given The Action is Hello # features/step_definitions/step_steps.rb:3
When The Subject is World # features/step_definitions/step_steps.rb:7
Then The Greeting is Hello1, World # features/step_definitions/step_steps.rb:11
expected: "Hello, World"
got: "Hello1, World" (using ==) (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/step_steps.rb:12:in `/The Greeting is (.*)/'
features/basic.feature:9:in `Then The Greeting is Hello1, World'
Failing Scenarios:
cucumber features/basic.feature:6 # Scenario: Hello World Scenario
1 scenario (1 failed)
3 steps (1 failed, 2 passed)
0m0.002s
这个就是我们需要的过程,红->绿,不过这里把这反了过来。