Blog | Phodal - A Growth Engineerhttp://www.phodal.com/blog/2021-11-08T12:06:19.378946+00:00Blog碎片粘合:Tasking DD 启发的思考2021-11-08T12:05:58+00:002021-11-08T12:06:19.378946+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/rethinking-in-tasking-dd/标题原来意指 TDD,即 Test Driven Development,用 TDD 来进行碎片化时间的粘合。只是呢,Tasking 才是 TDD 的核心,于是在新的思考之下,我重构了本文的大纲。这篇文章的构成也非常有意思 —— 以致于我都没想清楚,为什么会写成这样。它只是由一个个的思考,所构成的文章,有些杂乱。
## 引子 0:长长的 To Do
昨天打开我的 To Do 时,发现待写的“文章 idea”有近一百篇,大概率是写不完了。所以需要清理一下 Todo 了,本文也是其中的项。在日常工作中,总会突然领悟到一些东西(aha 时刻),便会简单地写个标题或者一句话记录一下。在周末的时候,寻找相关的资料,编写文章,以深入相关领域的思考。
在这里,我们的第一个关键点便是:随时可记录 idea。其次,便是寻找时间完善它。只要时间不长,就不会像我一样,有一个长长的 todo —— 够我再写一年了。
## 引子 1:成就感 ox Game
文章的另外一个起源非常有意思。源于如何在非工作时间摸鱼,如早、中、晚过度碎片的时候,如何有效地组织起来,~~以更好地玩游戏~~。我的意思是:写会代码,看点资料,补充技术。
当你的时间被切割得支离破碎,比如参加各种会议时,要再挤出时间来写点代码,并不是一件容易的事。想法被打断了,在刚找到感觉的时候,可能又一个新的会议,又或者是讨论。这大抵就是作为一个技术负责人的生活,能寻找稳定的编码时间非常不易。
接着,离开这个悲伤的故事。回想一下,每天茶余饭后,总会拿起手机,玩会游戏。游戏里有每日任务、每周任务、每月任务,休息的时候拣起来,在领导来的时候关上。
除了游戏会带来一系列的成就感之后,它还有一个明确地任务,让你知道你完成了没有,你可以看到与目标的差距。比如,你有一个 51 级的英雄,而这个游戏的上限是 60,所以很进步一点,就能看到明显地成长。也因此,与花时间成长来说,花时间玩游戏更有成就感。
## 引子 2:从 Test Driven 向上到 Tasking Driven
多年过去,我一直在努力地保持着早上、中午、下午编写代码。为了保证细粒度(10 ~ 20 分钟内)时间下,能提交一次代码。需要有更细粒度的需求/任务,才能达成这样的目标。
从实践的模式来看,TDD(Test Driven Development)是最适合于业余开源项目的实践 —— “随时”可以中断手头的工作,一旦另外的再优先级的任务完成之后,可以继续找到当前的任务(即测试),完成这部分的工作。尽管,依然会存在一定的状态丢失,但是都能尽量地回到上下文中。
在缺乏“周期性练习” + “刻意训练”的情况下,TDD 便是一件相当难的事情。它的难点不在于,是否先写测试,而在于 Tasking。你并不一定需要 Test First,而是需要 Tasking First(即先进行任务拆解)。依此往下类推,用于支撑我们改善碎片时间的一个关键是:Tasking Driven。
## 粘合碎片时间:动机 + 举措 + 拆解
从理论上,粘合起碎片时间,并不是一件复杂的事情。诸如于,日常工作时,通常在显示器前、电脑上粘贴各种便利贴,来提醒我们:今天需要做哪些事情?在解决了一个个的问题之后,便会干掉一个个的便利贴。与编码相比,这种粒度会更为相比。不过呢,它没有突然任务拆解的重要性。
所以,对于我们来说,会粘合起碎片化的时间,需要:
- 动机。强烈的动机,以实现某一愿景。
- 举措。实现愿景所需要做的举措
- 拆解。拆解完的任务的子任务
我们可以将它对比到软件开发中的看板与故事卡的故事,又或者是更高维度的精益价值树(LVT)。只是呢,上述的三者都具备一点的难度。
1. 如何有强烈的动机?假设我们想保护改善颈椎、腰椎的情况,那么正确的方式,应该是在工作的时候,多次起身做做动作。但是,什么时候才会让你有强烈的去攺它的欲望呢,当你的腰开始疼的时候……。
2. 怎样的举措才是合适的?再回到程序员健康这个问题,我怎么知道这个举措真的是有效的?我又从哪里获取对应的尝试性方案呢?来自社区网站(如知乎),又或者是朋友的建议?
3. 如何有效地拆解任务?采用 SMART 原则?我怎么验证拆解确实是有效的?
这部分的坑就暂时挖到了这里,等我回头再想想。而粘合起碎片时间,我觉得它的一个核心要素在于:状态回溯。
### 核心原则:状态回溯的机制
再回过来看,从 TDD 的例子里,我们有一个非常好的原则:IDE + Git 可以为我们提供一个非常好的状态记录。在我们离开 IDE 的时候,它记录下了当时的状态。只要你们的猫不会同时按下 Ctrl/Command + Z,再按一下其它的按钮,那么我们就可以回放到当时的状态。这一点点小的 IDE 功能,是人类的大脑无法提供的,它可以帮我们回溯时间线上记忆的瞬间。
所以,在我继续填碎片化时间的这个坑时,应该思考是否要做的其它事情,都有相应的回溯机制?
一个没有把握的例子,我们在读书的时候,往往是需要由几个连续的时间段组合而成。所以,每当我们再次拿起书本的时候,需要往前看看,以回到当时的状态。而如果我们在今天看完书里,能记录一下书上的内容,就能 GET 到当时的状态,它相当于是我们对于书的一种快照。这种快照的形态是多种多样的,如书的脑图,又或者是书的书评。
## 其它
我一直在佛性管理时间,从来没有好好计划过,什么时间做什么。只在状态的时候干活,不想干的时候就不干,或者瞎干。
所以,这里这是一点儿思考 —— 为了记录那些高优先级的思考,以及便于未来再拾起来看看。如何用 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 有点复杂。
如果没有的话,那么使用别人总结的方法,是一种更省事的方式。消费者驱动契约已死?2019-08-12T12:31:24+00:002019-08-12T06:31:53.504659+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/is-consumer-driven-contract-died/吐槽文一篇。
## 契约的一些问题
在实践前后端分离的这些年来,已经诞生了一些技术与工具让前后端进行沟通:
- 契约的 Mock 服务(Mock Server)。用于模拟一个服务器,为特定的接口返回特定的值。
- 契约测试。对前后端协定的 API 进行测试。
- 前后端胶水层。如 BFF (Backends for Frontends),根据客户端的需要对 API 进行聚合、适配、裁剪等。
而使用这些工具的时候,往往容易出现一些问题。
### 消费者无法驱动的契约
消费者驱动,顾名思义就是在我们协定契约的时候,按消费者的业务实现角度,与生产者一起协定契约。在实践上,往往由于后端在项目中的主导地位导致:协商的结果不会按消费者需要,而是按生者需要来生成契约。造成这种结果的因素有很多:
1. 后端才是实现方。所以,你常会听见:『你行,你上』,笑~。
2. 从实施成本考虑。诸如于跨服务调用的成本等等
3. 后端的职级高——毕竟后端已经有了几十年历史了。
4. 为了更『美』的后端架构——导致了更『差的』前端架构。
我的意思并不是说,按生产者来主导有什么问题,而是配不上『消费者驱动』这五个字。诸如于我们获取某个 API 的时候,前端所需要的是相关的信息,而不是对应的实体 ID。而后,再由前端去获取对应实体的信息。从逻辑上来说,由前端获取或者后端获取,从技术上来说,并没有多大的问题。但是从用户体验上来说,并没有那么友好。后端间的 API 调用,可以是几十 ms 级别的;而前端多调用一个后端 API,在网络上的传输时间,往往都是几百 ms,乃至几秒。
### 生产者的契约测试
所以,让我们再看看各个团队所宣称的**消费者驱动的契约测试**。
> 消费者驱动的契约测试(Consumer-Driven Contracts,简称CDC),是指从消费者业务实现的角度出发,驱动出契约,再基于契约,对提供者验证的一种测试方式。
如果你不是消费者驱动的契约,而是后端一口气定出来的接口,你怎么能叫『消费者驱动的契约测试』????按这样的做法,它只能叫生产者的契约测试。
既然是生产者的契约测试,那么它就无法保证说:API 修改不影响消费者使用——因为消费者压根就没有参与到契约的制定,生产者可以按照自己的需要来修改契约。
所以,生产者到底是在给谁测试?又是在测试什么东西?
### 契约测试的契约作为 Mock Server——不行
在大部分的项目中,对于契约的使用存在问题,即模式错了。**契约测试需要围绕业务流程施展,而不应该相互隔离**。如果契约的用例不丰富,便无法串起整个系统的流程。而作为 API 的提供方,应该保证业务逻辑是可以串联起来的。从用户的登录开始,到用户获取数据,展示列表页,能进入详情页等等。
我们做契约测试的目标是,让代码修改不影响 API 输出。后端需求一改,契约测试会挂,后端知道出现问题,会进行修复。如果前端用的是契约测试的契约提供的 Mock Server,前端的测试也会挂,这样问题就不会往后流。
所以,契约不应当给前端做本地开发和测试 。
## 没有契约的 Mock Server——不行
对于小团队来说,去采用原始的 API 方式反而更加有效率;对于大团队来说,这样的方式并不可行。
既然,沟通那么麻烦,那么我们就分开吧。如果契约和 Mock Server 是分开的,那么维护前端 Mock Server 的人很很难识别到修改。相关的问题,在测试不及时的情况下,有可能在上线后才发现。
也因此,从理想情况来说,这个 Mock Server 是契约动态生成的,并能根据后端现有最新的契约更新。或者,后端在实现契约测试时,要做的是只测试部分数据,而不是为了测试而测试。
### 只有 DDD 的微服务——不行
尽管 DDD 模型不一定非要与 API 绑定,但是由于种种原因,真正在实践的时候,就不是这么一回事。我们可以看到在很多的项目里,DDD 返回的实体资源,最后可能与 API 的字段一致的。但是它并非是业务上想要返回的逻辑,各个客户端(PC Web、小程序、Android 端、iOS 端等)还都需要进行二次的处理。
这就是为什么,DDD + 微服务,一定要配合一下 BFF 的原因。
### 缺少前端、后端的 BFF——不行
实施 BFF,大家并没有啥问题。可一谁来维护这一层胶水层的时候,大家都问题都来了——前后端都不愿意维护这个 BFF。对于前端来说,开发者只是 API 的消费者,突然间又变身成为生产者;对于后端来说,开发者原本不需要了解显示逻辑,现在也要涉及到这部分的内容。
不过呢,我们可以保持一致的是:**后端是 API 的生产者**。而如果生产者没有参与到 BFF 的开发,那么就会引发另外一个问题,API 发生变更的时候,可能没有同步到 BFF——尽管每个项目都有自己的规范,但是只要是由人来执行的,都可能会出现问题。
那么,我们就需要一个 BFF 的契约测试,那在第一时间知道 API 的变化。虽然感觉怪怪的,但是我们还是将客户端的契约测试,前移至了 BFF 层。
所以,消费者驱动的契约在哪里呢?
## 解决方案
### BFF
大家都懂了。
### 前端为主写 BFF
前端写 BFF 并不是一件容易的事。你要懂微前端,要懂主流的微服务框架,如 Spring Cloud,要懂 Java。与此同时,还需要了解各个 API 的变化 情况。对于前后端分离团队而言,要做样的工作太难了。
而尽管你可以采用 Node.js + TypeScript,但是它也意味着你要成为一个 Node.js 专家。如果让我用 Node.js 写 BFF,那我还是宁愿和以前的项目一样,写 Scala 来作为 BFF。
所以,我并不推荐使用 Node.js 作为 BFF。一来,没有阿里巴巴强大的 Node.js 专家群;二来,我对于成为 Node.js 没有兴趣。用擅长于某一领域的语言去做某一领域的事,而非用擅长的语言去做每一领域的事——兴趣和爱好除外。
### GraphQL as BFF
大家都懂了。
### 前端防腐层
参考《整洁前端架构》。
### 消费者的契约测试
我们在诸多项目实现了:**针对于生产者的契约测试**。其中也包含了之前我写的 ``mest`` 框架,它是通过结合 TypeScript 的 Interface 来验证后端接口是否一致。
当你没有办法的时候,这也是一个不那么差的做法。换个问题来思考的话,就是以生产者来解决这些问题。
直接转换模型为 Java Class 和 TypeScript 是一种更简单的做法。
## 结论
没有银弹。Serverless 架构应用开发:如何编写 Serverless 应用的测试2017-11-10T11:35:58.901368+00:002017-11-10T11:35:58.956219+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/serverless-architecure-development-write-serverless-application-test/如 Serverless Framework 官方所说
虽然 Serverless 架构在服务业务逻辑方面引入了很多简单性,但是它的一些特性给测试带来了挑战。他们是:
- Serverless 架构是独立的分布式服务的集成,它们必须被独立地和一起地测试。
- Serverless 架构依赖于互联网、云服务,这些服务很难在本地模拟。
- Serverless 架构可以具有事件驱动的异步工作流程,这些工作流程很难完全仿真。
因此官方建议:
- 编写您的业务逻辑,使其与 FaaS 提供商(例如,AWS Lambda)分开,以保持提供者独立性,可重用性和更易于测试。
- 当您的业务逻辑与FaaS提供商分开编写时,您可以编写传统的单元测试以确保其正常工作。
- 编写集成测试以验证与其他服务的集成是否正常工作。
Serverless 应用的测试
---
在传统的测试金字塔里,我们会写更多的单元测试,并尽可能地尽少集成测试。同样的,在 Serverless 架构应用里,我们会写同样数量的单元测试,只是会写更多地集成测试,用于测试与服务间的集成。而这些测试,往往更加依赖于网络,并且这些测试越需要我们隔离架构层级。
因而,这种情况下,我们需要在测试上花费更多的精力。
对于单元测试来说,在之前的 [Express 示例](https://www.phodal.com/blog/serverless-development-guide-express-react-build-server-side-rendering/)里,我们做了一个不错的 Demo。我们隔离了 Express 与 Lambda 函数之间的代码,只需要再加上一个本地的 Express 环境,那么我们就可以直接在本地运行了。而借助于上一篇中提供的 [serverless-offline](https://www.phodal.com/blog/serverless-architecture-development-serverless-offline-localhost-debug-test/),我们则可以隔离本地数据库。
随后,我们需要将 Serverless 应用部署测试环境。然后运行我们的测试脚本,自动地打开浏览器,进行操作。然后验证数据库中的数据是否是正确的,而一些都依赖于网络来执行。这就意味着,我们仿佛在不断地对接第三方系统,看上去就像一场场的恶梦。好在,我们也可以在 AWS 上运行测试,至少会让网络问题变得好一些。
步骤
---
在这里,我们要用 serverless-mocha-plugin 插件,这是一个基于 Mocha 框架、用于为 Serverless Framework 的添加测试的插件。
它的 Setup 过程非常简单,先添加插件:
```
yarn add --dev serverless-mocha-plugin
```
随后,添加到 ``serverless.yml`` 文件中:
```
plugins:
- serverless-mocha-plugin
```
接着,我们就可以创建测试了。
### 创建测试
除了运行测试,它还提供创建测试的命令。只需要运行命令
```
sls create test -f functionName
```
如,在这里我们是这样的:
```
$ sls create test -f hello
Serverless: serverless-mocha-plugin: created test/hello.js
```
其文件的内容如下:
```
'use strict';
// tests for hello
// Generated by serverless-mocha-plugin
const mochaPlugin = require('serverless-mocha-plugin');
const expect = mochaPlugin.chai.expect;
let wrapped = mochaPlugin.getWrapper('hello', '/handler.js', 'hello');
describe('hello', () => {
before((done) => {
done();
});
it('implement tests here', () => {
return wrapped.run({}).then((response) => {
expect(response).to.not.be.empty;
});
});
});
```
如果你写过 Mocha 测试的话,那么你应该能看懂上面的代码。
### 运行测试
现在,我们就可以运行测试了,命令以以下的格式运行:
```
sls invoke test [--stage stage] [--region region] [-f function1] [-f function2] [...]
```
我们也可以直接运行所有的测试:
```
$ sls invoke test
hello
✓ implement tests here
1 passing (7ms)
```
### 更准确的测试
让我们再让测试有针对性一点。在 ``handler.js`` 中,我们返回的 body 是一个字符串:
```
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}),
};
```
那么,我们就应该去测试一下相应的字符串:
```
it('implement tests here', () => {
return wrapped.run({}).then((response) => {
let body = JSON.parse(response.body);
expect(body.message).equal('Go Serverless v1.0! Your function executed successfully!');
});
});
```
结论
---
总的来说,对于普通的单元测试来说,和一般的测试差不多。对于数据库操作什么相关的函数来说,这就是一件复杂的事。如何用测试驱动出100%测试覆盖率的代码2016-06-22T14:43:28+00:002016-06-22T14:45:55.308946+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/use-tdd-drive-100-percent-test-coverage/本文以[DDM](https://github.com/phodal/ddm)为例,简单地介绍一下如何用测试驱动开发(TDD, Test-Driven Development)的方法来驱动出这个函数库。
### DDM简介
DDM是一个简洁的前端领域模型库,如我在《[DDM: 一个简洁的前端领域模型库](https://www.phodal.com/blog/ddm-simple-domain-model-in-frontend/)》一文中所说,它是我对于DDD在前端领域中使用的一个探索。
简单地来说,这个库就是对一个数据模型的操作——增、删 、改,然后生成另外一个数据模型。
![DDM][1]
如以Blog模型,删除Author,我们就可以得到一个新的模型。而实现上是因为我们需要RSS模型,我们才需要对原有的模型进行修改。
预先式设计
---
如果你对TDD有点了解的话,那么你可能会预先式设计有点疑问。
等等,什么是测试驱动开发?
> 测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。
流程大概就是这样的,先写个测试 -> 然后运行测试,测试失败 -> 让测试通过 -> 重构。
![enter image description here][2]
换句简单的话来说,就是 红 -> 绿 -> 重构。
在DDM项目里,就是一个比较适合TDD的场景。我们知道我们所有的功能,我们也知道我们需要对什么内容进行测试,并且它很简单。因为这个场景下,我们已经知道了我们所需要的功能,所以我们就可以直接设计主要的函数:
```javascript
export class DDM {
constructor() {}
from() {};
get(array) {};
to() {};
handle() {};
add() {};
remove(field) {};
}
```
上面的就是我们的需要函数,不过在后来因为需要就添加了``replace``和``replaceWithHandle``方法。
然后,我们就可以编写我们的第一个测试了。
第一个驱动开发的测试
---
我们的第一个测试,比较简单,但是也比较麻烦——我们需要构建出基本的轮廓。我们的第一个测试就是要测试我们可以从原来的对象中取出title的值:
```javascript
let ddm = new DDM();
var originObject = {
title: 'hello',
blog: 'fdsf asdf fadsf ',
author: 'phodal'
};
var newObject = {};
ddm
.get(['title'])
.from(originObject)
.to(newObject);
expect(newObject.title).toBe("hello");
```
对应的,为了实现这个需要基本的功能,我们就可以写一个简单的return来通过测试。
```javascript
from(originObject) {
return this;
};
get(array) {
return this;
};
to(newObject) {
newObject.title = 'hello';
return this;
};
```
但是这个功能在我们写下一个测试的时候,它就会出错。
```javascript
ddm
.get(['title', 'phodal'])
.from(originObject)
.to(newObject);
expect(newObject.title).toBe("hello");
expect(newObject.author).toBe("phodal");
```
但是这也是我们实现功能要做的一步,下一步我们就可以实现真正的功能:
- 在from函数里,复制originObject
- 在get函数里,获取新的对象所需要的key
- 最后,在to函数里,进行复制处理
```javascript
from(originObject) {
this.originObject = originObject;
return this;
};
get(array) {
this.newObjectKey = array;
return this;
};
to(newObject) {
for (var key of this.newObjectKey) {
newObject[key] = this.originObject[key];
}
return this;
};
```
现在,我们已经完成了基本的功能。
一个意外的情况
---
在我实现的过程中,我发现如果我传给get函数的array如果是空的话,那么就不work了。于是,就针对这个情况写了个测试,然后实现了这个功能:
```javascript
get(keyArray) {
if(keyArray) {
this.newObjectKey = keyArray;
} else {
this.newObjectKey = [];
}
return this;
};
to(newObject) {
if(this.newObjectKey.length > 0){
for (var key of this.newObjectKey) {
newObject[key] = this.originObject[key];
}
} else {
// Clone each property.
for (var prop in this.originObject) {
newObject[prop] = clone(this.originObject[prop]);
}
}
return this;
};
```
在这个过程中,我还找到了一个clone函数,来替换from中的"="。
```javascript
from(originObject) {
this.originObject = clone(originObject);
return this;
};
第三个驱动开发的测试
---
因为有了第一个测试的基础,我们要写下一测试变得非常简单:
```javascript
dlm.get(['title'])
.from(originObject)
.add('tag', 'hello,world,linux')
.to(newObject);
expect(newObject.tag).toBe("hello,world,linux");
expect(newObject.title).toBe("hello");
expect(newObject.author).toBe(undefined);
```
在实现的过程中,我又投机取巧了,我创建了一个对象来存储新的对象的key和value:
```javascript
add(field, value) {
this.objectForAddRemove[field] = value;
return this;
};
```
同样的,在``to``方法里,对其进行处理:
```javascript
to(newObject) {
function cloneObjectForAddRemove() {
for (var prop in this.objectForAddRemove) {
newObject[prop] = this.objectForAddRemove[prop];
}
}
function cloneToNewObjectByKey() {
for (var key of this.newObjectKey) {
newObject[key] = this.originObject[key];
}
}
function deepCloneObject() {
// Clone each property.
for (var prop in this.originObject) {
newObject[prop] = clone(this.originObject[prop]);
}
}
cloneObjectForAddRemove.call(this);
if (this.newObjectKey.length > 0) {
cloneToNewObjectByKey.call(this);
} else {
deepCloneObject.call(this);
}
return this;
};
```
在这个函数里,我们用cloneObjectForAddRemove函数来复制将要添加的key和value到新的对象里。
remove和handle函数
---
对于剩下的remove和handle来说,他们实现起来都是类似的:
- 存储相应的对象操作
- 然后在to函数里进行处理
编写测试:
```javascript
function handler(blog) {
return blog[0];
}
ddm.get(['title', 'blog', 'author'])
.from(originObject)
.handle("blog", handler)
.to(newObject);
expect(newObject.blog).toBe('A');
```
然后实现功能:
```javascript
remove(field) {
this.objectKeyForRemove.push(field);
return this;
};
handle(field, handle) {
this.handleFunction.push({
field: field,
handle: handle
});
return this;
}
```
这一切看上去都很自然,然后我们就可以对其进行重构了。
100%的测试覆盖率
---
由于,我们先编写了测试,再实现代码,所以我们编写的代码都有对应的测试。因此,我们可以轻松实现相当高的测试覆盖率。
在这个Case下,由于业务场景比较简单,要实现100%的测试覆盖率就是一件很简单的事。
(PS: 我不是TDD的死忠,只是有时候它真的很美。)
[1]: /static/media/uploads/ddm.png
[2]: /static/media/uploads/tdd_cycle.jpg《Python Web开发 - 测试驱动方法》阅后感2015-10-27T12:54:19+00:002015-10-27T13:12:00.382117+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/after-python-web-tdd-with-django/这本书的原名是叫《Test-Driven Development with Python》,小标题是 **Obey the Testing Goat: Using Django, Selenium, and JavaScript**。虽然有点难以理解为何这本书的中文名变成了《Python Web开发 - 测试驱动方法》,总感觉怪怪的,毕竟Kent Beck的那本书名是《测试驱动开发》。
如我在微博上所说,这本书的Python Web开发所用的框架是Django。问了几个出版社都没有出版Django书的计划,要知道有这么多公司使用了Django:
![Who use Django][1]
尽管最近几年里Flask似乎比Django受欢迎,但是Django是一个设计得非常巧妙的框架。而且,越来越多的公司开始使用Django替换他们原有的系统,如Firefox所在的Mozilla。吐槽完毕,让我们进入正题。
**书的作者在一家使用敏捷开发的软件公司里**。
如果你想(复制一下小结):
1. 学习自动测试(针对Developer)
2. 测试驱动开发
3. 持续交付
那么,这本书是非常值得看的。
##测试
这本书的主要话题自然是测试了。
###TDD(测试驱动开发)
传统的软件公司的测试和开发是分离的,这就意味着你并不需要写你的功能测试。由于,没有在那样的公司工作过,我也不知道他们是否写单元测试。反正我所在的公司,单元测试和功能测试都是要写的。但是,我相信他们需要有个三次握手的过程:
![三次握手][2]
这就有点像开发团队和产品团队在互相推诿责任,“你们的需求实现不了”,“你们开发的东西有问题”。对于产品来说,最好的过程莫过于产品团队和开发团队一起开发实现功能。同比,如果你的测试和产品代码是分开写的,如果你**不打算改变现状、走出舒适区或者尝试新的东西**,那么你不需要TDD,你也不需要这本书。
So,这本书的大部分内容都是关于如何展开TDD的。
###自动测试
现在,有一个新的项目来了,客户想到一个TO-DO List。TDD的第一要素是测试,所以先用Selenium来了一个单元测试,用于测试首页是存在的,并且标题中含有Django。
```python
from selenium imoprt webdriver
browser = webdriver.Firefox()
browser.get('http://localhost:8000')
assert 'Django' in browser.title
```
你可能已经猜到结果了,这个测试必须是挂的。如果你没有用到Selenium这样的自动测试工具,你应该试试,它会在你运行的时候,自动打开浏览器。
![Selenium Firefox][3]
因为Web服务并没有启动,所以你需要用**django-admin.py**去创建一个项目,然后就是经典的Hello,World。
如果你写过Java或者Python等等的测试,你可能已经猜到了。你写的测试都会自动的执行下去,所以他会把这些任务一个个跑一遍。并且,是由机器来执行:
1. Selenium会打开一个浏览器窗口,打开某个页面,输入表单信息,然后点击确认,最后验证信息是不是正确的。
2. Selenium会打开一个浏览器窗口,打开某个页面查看页面的标题是不是正确的。
3. Selenium会打开一个浏览器窗口,并按照你的需要点击页面的某个button,然后检查页面是不是会出现类似的东西。
![contribute][4]
而你并没有实现这些功能,所以你需要去实现他们。
然后这本书就在重复上面的过程,过程中你学会了怎么使用Django。但是,你并不会意识到这其中的美妙之处。
###红-绿-重构
实现上,我们在重复的过程是:红-绿-重构。
通常来说,红的原因是因为我们依据客户需求编写测试用例。接着,通过让测试变绿(成功),我们就知道我们实现了这个功能。如果你的功能代码写得很好,那么你不就需要去重构代码了。所以,其实重构代码的前提是你已经有了测试,而TDD就是在保证你有测试来cover功能代码。
所以,如果你所在的项目之所以没有人敢重构,就是因为测试覆盖率不够。
###测试小结
接着,作者对实践过程中遇到的问题进行了一些总结。如测试速度、拆分测试、什么时候使用集成测试(书中翻译为整合测试),这些小结相当重要。像在我们的项目中,运行所有的测试大概要半个小时,这期间不断跳到的Firefox浏览器(~10个)会夺走你对电脑的使用权。
好了,这本书2/3的话题已经完了。
##持续交付
这是这本书另外1/3的话题划分到了持续交付这样的话题,当然这只是我对他们的总结。
持续交付意味着几个话题,如持续集成、持续发布、自动部署,所以书中提到了几个不错的软件:
1. Fabric,一个用于自动部署的工具 —— Python语言。
2. HTTP服务器Nginx。
3. WSGI服务器Gunicorn
4. Jenkins,持续集成(CI)。虽然我们项目上用的是Bamboo,但是他们都是持续集成构建服务器软件。这意味着,在你PUSH代码后,在CI上会安装依赖、运行测试、发布版本等等。
(ps: 我的博客就是 Django + Nginx + Gunicorn + Mezzanine (CMS) + Fabric (Mezzanine自带),但是没有Jenkins)
忘说了,上面的所有内容都是敏捷的开发流程。
##小结
So,So,如果你想:
1. 学习自动测试
2. 测试驱动开发
3. 持续交付
那么,这本书是非常值得看的。
如果你不感兴趣,那么就送给我吧。
[1]: /static/media/uploads/who-use-django.jpg
[2]: /static/media/uploads/third-times.png
[3]: /static/media/uploads/firefox-tdd.jpg
[4]: /static/media/uploads/contribute.png《RePractise》: 一次测试驱动开发2015-08-11T13:49:56+00:002015-08-11T13:51:10.042944+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/think-about-tdd/#《RePractise》: 一次测试驱动开发
虽然接触的TDD时间不算短,然而真正在实践TDD上的时候少之又少。除去怎么教人TDD,就是与人结对编程时的switch,或许是受限于当前的开发流程。
偶然间在开发一个物联网相关的开源项目——[Lan](https://github.com/phodal/lan)的时候,重拾了这个过程。不得不说提到的一点是,在我们的开发流程中**测试是由相关功能开发人员写的**,有时候测试是一种很具挑战性的工作。久而久之,为自己的开源项目写测试变成一种自然而然的事。有时没有测试,反而变得**没有安全感**。
##一次测试驱动开发
###故事
之前正在重写一个[物联网](http://www.phodal.com/iot)的服务端,主要便是结合CoAP、MQTT、HTTP等协议构成一个物联网的云服务。现在,主要的任务是集中于协议与授权。由于,不同协议间的授权是不一样的,最开始的时候我先写了一个http put授权的功能,而在起先的时候是如何测试的呢?
curl --user root:root -X PUT -d '{ "dream": 1 }' -H "Content-Type: application/json" http://localhost:8899/topics/test
我只要顺利在request中看有无``req.headers.authorization``,我便可以继续往下,接着给个判断。毕竟,我们对HTTP协议还是蛮清楚的。
if (!req.headers.authorization) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
return res.end('Unauthorized');
}
可是除了HTTP协议,还有MQTT和CoAP。对于MQTT协议来说,那还算好,毕竟自带授权,如:
mosquitto_pub -u root -P root -h localhost -d -t lettuce -m "Hello, MQTT. This is my first message."
便可以让我们简单地完成这个功能,然而有的协议是没有这样的功能如CoAP协议中是用Option来进行授权的。现在的工具如libcoap只能有如下的简单功能
coap-client -m get coap://127.0.0.1:5683/topics/zero -T
于是,先写了个测试脚本来验证功能。
var coap = require('coap');
var request = coap.request;
var req = request({hostname: 'localhost',port:5683,pathname: '',method: 'POST'});
...
req.setHeader("Accept", "application/json");
req.setOption('Block2', [new Buffer('phodal'), new Buffer('phodal')]);
...
req.end();
写完测试脚本后发现不对了,这个不应该是测试的代码吗? 于是将其放到了spec中,接着发现了上面的全部功能的实现过程为什么不用TDD实现呢?
###说说测试驱动开发
测试驱动开发是一个很"古老"的程序开发方法,然而由于国内的开发流程的问题——即开发人员负责功能的测试,导致这么好的一项技术没有在国内推广。
测试驱动开发的主要过程是:
1. 先写功能的测试
2. 实现功能代码
3. 提交代码(commit -> 保证功能正常)
4. 重构功能代码
而对于这样的一个物联网项目来说,我已经有了几个有利的前提:
1. 已经有了原型
2. 框架设计
###思考
通常在我的理解下,TDD是可有可无的。既然我知道了我要实现的大部分功能,而且我也知道如何实现。与此同时,对Code Smell也保持着警惕、要保证功能被测试覆盖。那么,总的来说TDD带来的价值并不大。
然而,在当前这种情况下,我知道我想要的功能,但是我并不理解其深层次的功能。我需要花费大量的时候来理解,它为什么是这样的,需要先有一些脚本来知道它是怎么工作的。TDD变显得很有价值,换句话来说,在现有的情况下,TDD对于我们不了解的一些事情,可以驱动出更多的开发。毕竟在我们完成测试脚本之后,我们也会发现这些测试脚本成为了代码的一部分。
在这种理想的情况下,我们为什么不TDD呢?用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>mock stub与tdd2014-01-04T15:19:58+00:002014-05-13T17:27:20.534254+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/mock-vs-stub-in-tdd/要区分stub与mock在缺少编程实践经验的时候有些困难。
- mock的意思是模仿
- stub的意思是存根
##不同的对比##
###模仿与简单替换###
- Stub是代码的一部分。其目的就是用简单的行为替换复杂的行为,从而允许独立地测试代码的一部分。
- Mock Object是使用来代替与你的代码协作的对象的对象,这样代码可以调用Mock Object的方法,这些方法的调用的结果是由你的测试设置好的。
###记录 验证预期###
- Mock objects are used to define expectations i.e: In this scenario I expect method A() to be called with such and such parameters. Mocks record and verify such expectations.
- Stubs, on the other hand have a different purpose: they do not record or verify expectations, but rather allow us to “replace” the behavior, state of the “fake”object in order to utilize a test scenario
中文大意
- Mock对象用于定义预期,即:在这种情况下,我期望方法A()能在带这样或那样的参数的调用。Mock记录并验证这样的期望。
- Stub,在另一方面有不同的目的:他们并不记录或验证预期,而是允许我们在替换 fake对象的行为,以便利用测试一个测试场景。
- A mock is something that as part of your test you have to setup with your expectations. A mock is not setup in a predetermined way so you have code that does it in your test. Mocks in a way are determined at runtime since the code that sets the expectations has to run before they do anything.
- Tests written with mocks usually follow an initialize-> set expectations -> exercise > verify pattern to testing. While the pre-written stub would follow an initialize -> exercise -> verify. The purpose of both is to eliminate testing all the dependencies of a class or function so your tests are more focused and simpler in what they are trying to prove.
##理解##
###stub###
- Stub是一种状态确认
- Stub用简单的行为来替换复杂的行为
###mock###
- Mock是一种行为确认
- Mock模拟行为
###stub && mock###
- Stub从某种程度上来说,会返回我们一个特定的结果,用代码替换来方法。
- Mock确保这个方法被调用。
###理解下的stub与mock###
stub从字面意义上来说是存根,存根可以理解为我们保留了一些预留的结果。这个时候我们相当于构建了这样一个特殊的测试场景,用于替换诸如网络或者IO口调度等高度不可预期的测试。如当我们需要去验证某个api被调用并返回了一个结果,举例在最小物联网系统设计中返回的json,我们可以在本地构建一个[{"id":1,"temperature":14,"sensors1":15,"sensors2":12,"led1":1}]的结果来当我们预期的数据,也就是所谓的存根。那么我们所要做的也就是解析json,并返回预期的结果。当我们依赖于网络时,此时测试容易出现问题。
mock从字面意义上来说是模仿,也就是说我们要在本地构造一个模仿的环境,而我们只需要验证我们的方法被调用了。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
这个就是我们需要的过程,红->绿,不过这里把这反了过来。ThoughtWorks郑大晔校的一天(3)——Javascript的TDD,Jasmine2013-12-28T21:54:08+00:002014-05-14T02:13:20.279523+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/zheng-da-ye-xiao-of-Thoughtworks-three-tdd-javascript-jasmine/似乎我应该第二次的内容补上,只是不知道写什么了,也就没有必要写了,不过这次算是有必要了。看了一些敏捷开发的书,在想是不是也有Javascript也要有个测试框架,也会有类似于Ruby的Rspec,java的Junit。
不过,似乎我印象里有个Qunit,和jQuery是一家子的,这个留给过些时候学吧,因为今天讲到了Jasmine和RSpec和有点像。
##Rspec以及Jasmine##
来些Rspec的测试代码吧
<pre><code class="ruby">
describe LedStatus do
let(:ledstatus){LedStatus.new()}
describe "Observable" do
it "Should have a result" do
led=Led.new
end
end
end
</code></pre>
我似乎把代码写得有点不理想了,但是这个不是很重要,应该不是很重要。。似乎真的不是很重要。。
测试一下
rspec --format d
会有如下结果:
LedStatus
Observable
Should have a result
Finished in 0.00065 seconds
1 example, 0 failures
然后,我们先看看jasmine的测试结果
Jasmine2.0.0 finished in 0.003s
1 spec, 0 failures
Add function
should be return
对了,忘了说一下Jasmine的测试
##Jasmine##
<blockquote>Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.</blockquote>
似乎这个是BDD,不过已经无关紧要了,因为Rspec也是,似乎我没有理解好。
官网地址 [http://pivotal.github.io/jasmine/][1]
直接下载官方的示例我们可以看到我们的运行结果。
接着我们创建一下简单的add函数,这个是直接从课堂上拉过来的。。
<pre><code class="javascript">
describe("Add function", function() {
it("should be return", function() {
var result=add(1,2);
expect(result).toEqual(3);
});
});
</code></pre>
SimpleSpec.js的内容
下面是Simple.js的代码
function add(num1,num2) {
return num1+num2;
}
似乎很简单明了,再打开浏览器就可以看到结果了。。
呼呼,这就是Javascript的TDD。。
##总结##
原来道路一直很长,只是不会再那么艰辛。
###写在技巧之后###
似乎今天在讲最小物联网系统的时候没有讲好,部分原因是因为自己有些紧张,只是讲的时候似乎就没有紧张,但是就一直讲了下去。。用TWer的话来说的话,就是今天很赞。
[1]:http://pivotal.github.io/jasmine/