从蓝海战略到长尾理论_从战

从蓝海战略到长尾理论

验收测试驱动开发入门

您是否曾经遇到过这种情况:

然后,本文为您服务-一个具体示例,说明如何开始在现有代码库上进行验收测试驱动的开发。 这是解决技术债务的一部分

这是一个包含疣和所有东西的真实示例,而不是精美的教科书示例。 所以穿上你的战靴。 我将只使用Java和Junit,而不会使用任何花哨的第三方测试框架(这些框架往往会被过度使用)。

免责声明:我并不是说这是正确的方法,还有ATDD的许多其他“风味”。 此外,本文中没有太多新的或创新的内容,这只是行之有效的实践和来之不易的经验。

我想做什么

几天前,我坐下来为webwhiteboard.com (我的宠物项目)构建了一个密码保护功能。 长期以来,人们一直在寻求一种用密码保护其在线白板的方法,因此是时候将其完成。

听起来像是一个简单的功能,但是要做出很多设计决策。 到目前为止,webwhiteboard.com一直基于匿名使用,并且没有任何类型的帐户,登录名或密码。 谁应该能够保护白板? 谁应该可以访问它? 如果忘记密码怎么办? 我们如何使事情简单但足够安全?

Webwhiteboard代码库具有不错的单元测试和集成测试范围。 但是它没有验收测试。 也就是说,从用户的角度来看,这些测试要经过端到端的流程。

设计注意事项

Web白板的主要设计目标是简化:最大程度地减少对登录和帐户以及其他烦恼的需求。 因此,我为密码功能设置了两个设计约束:

  • 在白板上设置密码将需要用户身份验证,但访问受密码保护的面板则不需要。 也就是说,打开受保护的白板的用户需要输入白板密码,而无需“登录”。
  • 登录将使用第三方OpenId / Oauth服务提供商(最初是Google)进行。 这样,用户不必创建另一个用户帐户。

实施方法

这里有很多不确定性。 我不确定我希望它如何工作,甚至不确定我想要如何实现它。 所以这是我的方法(基本上是ATDD):

  • 步骤1:高层次记录预期的流程
  • 步骤2:将其转换为可执行的验收测试
  • 步骤3:运行验收测试,但失败。
  • 步骤4:使验收测试成功。
  • 步骤5:清理代码

这是反复进行的,因此在每个步骤中,我都可以决定返回并调整上一个步骤(我经常这样做)。

步骤1:记录预期的流程

假设功能已完成。 我睡着的时候一个天使来了并实施了它。 听起来好得令人难以置信! 我将如何验证? 我要手动测试的第一件事是什么? 这个:

  1. 我创建一个新的白板
  2. 我在上面设置了密码。
  3. 乔试图打开我的白板,要求输入密码。
  4. Joe输入了错误的密码,并被拒绝访问
  5. Joe再次尝试,输入正确的密码并获得访问权限。 (“ Joe”当然就是我,使用另一个Web浏览器…)。

当我编写这个小测试脚本时,我意识到要考虑很多替代流程。 但这是主要情况。 如果我能解决这个问题,那我就走了。

步骤2:将其转换为可执行的验收测试

这是棘手的部分。 我没有其他的2端到2端验收测试,那么我该如何开始? 该功能将通过第三方认证系统(我的初步决定是使用Janrain)以及数据库进行交互,并有大量的参与弹出对话框和令牌和重定向和这种棘手的网页内容。 啊。

是时候退后一步了。 在解决“我如何编写此验收测试”问题之前,我需要解决一个更基本的问题“在此代码库中我该如何编写验收测试?”

为了提出这个问题,我试图确定我可以测试的“最简单的功能”,而今天该功能已经可以使用。

步骤2.1编写最简单的可执行验收测试

这是我想出的:

  1. 尝试打开一个不存在的白板
  2. 检查我没有得到白板

我将如何实施此测试? 哪些框架? 哪些工具? 它应该包含GUI还是绕开它? 它应该包含客户端代码还是直接与服务器对话?

很多问题。 诀窍是:不要回答他们! 只是假装一切都以某种方式精美地解决了,然后将测试编写为伪代码。 这里是。


公共 AcceptanceTest {
@测试
公共 无效 openWhiteboardThatDoesntExist(){
// 1。 尝试打开一个不存在的白板
// 2。 检查我没有得到白板
}
}

我运行它,它成功了! 欢呼! 呃,别等,那是错的! TDD三角形中的第一步(“红绿色重构”)是Red 。 因此,我需要使其失败,以证明需要构建该功能。

我最好继续编写一些真实的测试代码。 但是,伪代码使我朝着正确的方向前进。

步骤2.2将最简单的验收测试设为红色。

为了使该测试成为现实,我组成了一个名为AcceptanceTestClient的类,并假装它神奇地解决了所有问题,并为我提供了一个漂亮的高级API来运行我的验收测试。 使用它是如此简单:

客户端 .openWhiteboard( “ xyz” );
assertFalse( 客户端 .hasWhiteboard());

在编写该代码时,实际上是在发明一种适合该测试用例确切需求的API。 它应该与伪代码一样多。

接下来,我在Eclipse中使用快捷键使它自动生成一个空版本的AcceptanceTestClient及其所需的方法:


公共 AcceptanceTestClient {
public void openWhiteboard(String string){
// TODO自动生成的方法存根
}

public boolean hasWhiteboard(){
// TODO自动生成的方法存根
返回 false ;
}
}

现在是完整的测试类:


公共 AcceptanceTest {
AcceptanceTestClient 客户 ;

@测试
公共 无效 openWhiteboardThatDoesntExist(){
// 1。 尝试打开一个不存在的白板
客户端 .openWhiteboard( “ xyz” );

// 2。 检查我没有得到白板
assertFalse( 客户端 .hasWhiteboard());
}
}

该测试运行,但是失败(因为client为null)。 好!

我解决了什么? 不多。 但这是一个开始。 我有一个验收测试帮助程序类的开始,即AcceptanceTestClient。

步骤2.3。 使最简单的验收测试成为绿色

下一步是使验收测试变成绿色。

请注意,我现在要解决一个更简单的问题。 我不必担心身份验证以及多个用户和诸如此类的问题。 我可以稍后再添加测试。

至于AcceptanceTestClient,实现是非常标准的-模拟数据库(我已经有代码了)并运行整个Webwhiteboard系统的内存版本。

这是生产设置:

(点击图片放大)

技术细节:Web Whiteboard使用GWT(Google Web工具包)。 一切都是用Java编写的,但是GWT会自动将客户端代码转换为javascript,并插入RPC魔术(远程过程调用)以封装异步客户端-服务器通信的所有肮脏细节。

在验收测试设置,我“短路”系统并切断了所有的框架, 第三方服务和网络通信。

(点击图片放大)

因此,我创建了一个AcceptanceTest客户端,该客户端以与真实客户端相同的方式与Web白板服务对话。 区别在于窗帘。

  • 真正的客户与Web白板服务接口进行通信,并且该接口在GWT环境中运行,该环境会自动将请求转换为RPC调用并中继到服务器。
  • 验收测试客户端还与Web白板服务界面进行通信,但是该客户端直接连接到本地服务实现,不需要RPC,因此在运行测试时不需要GWT。

同样,在验收测试配置中,它将用伪造的内存数据库替换mongo数据库(基于云的NoSQL数据库)。

所有这些伪造的原因是为了简化环境,使测试运行更快,并确保测试正在测试与所有框架和网络事物隔离的业务逻辑。

这听起来像是一个复杂的设置,但实际上,它实际上只是一个包含3行的init方法。


公共 AcceptanceTest {
AcceptanceTestClient 客户 ;

@之前
公共 无效 initClient(){
WhiteboardStorage fakeStorage = 新的 FakeWhiteboardStorage();
WhiteboardService服务= 新的 WhiteboardServiceImpl(fakeStorage);
客户端 = 新的 AcceptanceTestClient(service);
}

@测试
公共 无效 openWhiteboardThatDoesntExist(){
客户端 .openWhiteboard( “ xyz” );
assertFalse( 客户端 .hasWhiteboard());
}
}

WhiteboardServiceImpl是Web白板系统的现有服务器端实现。

请注意,AcceptanceTestClient现在在其构造器中接受WhiteboardService实例(一种模式称为“依赖注入”)。 这给我们带来了额外的副作用:它不在乎配置。 只需发送实时配置的WhiteboardService实例,即可将相同的未修改AcceptanceTestClient类用于在实时环境中进行测试。


公共 AcceptanceTestClient {
私有 最终 WhiteboardService 服务 ;
专用 WhiteboardEnvelope 信封 ;

公共 AcceptanceTestClient(WhiteboardService服务){
这个服务 =服务;
}

public void openWhiteboard(String whiteboardId){
boolean createIfMissing = false ;
这个信封 = 服务 .getWhiteboard(whiteboardId,createIfMissing);
}

public boolean hasWhiteboard(){
返回 信封 != null ;
}
}

因此,总而言之,AcceptanceTestClient模仿了真正的Web白板客户端所做的事情,同时为接受测试提供了高级API。

您可能想知道“当我们已经有可以直接与之对话的WhiteboardService时,为什么为什么需要一个AcceptanceTestClient?”。 有两个原因:

  1. WhiteboardService API更底层。 AcceptanceTestClient完全实现验收测试所需的方法,并以使它们尽可能易于阅读的确切方式实现。
  2. AcceptanceTestClient隐藏了测试代码不需要的内容,例如WhiteboardEnvelope的概念,createIfMissing布尔值和其他较低级别的细节。 实际上,还涉及更多服务,例如UserService和WhiteboardSyncService。

我不会为您带来更多关于AcceptanceTestClient代码的细节,因为本文不是关于Web白板的内部管道。 可以说,AcceptanceTestClient将接受测试的需求映射到与白板服务接口进行交互的底层细节。 这很容易实现,因为真实的客户端代码可以有效地充当“如何与服务交互”教程。

无论如何,现在我们最简单的验收测试通过了!


@测试
公共 无效 openWhiteboardThatDoesntExist(){
myClient .openWhiteboard( “ xyz” );
assertFalse(myClient .hasWhiteboard());
}

下一步是清理一下。

实际上,我没有为此编写任何生产代码(因为该功能已经存在并且可以正常使用),它只是测试框架代码。 但是,尽管如此,我还是花了几分钟的时间清理,删除重复项,使方法名称更清晰等。

最后,出于完整性考虑,我又添加了一个测试,因为它很简单:o)


@测试
公共 无效 createNewWhiteboard(){
客户端 .createNewWhiteboard();
assertTrue( 客户端 .hasWhiteboard());
}

欢呼,我们有一个测试框架! 而且,我们甚至不需要任何精美的第三方库。 只是Java和Junit。

步骤2.4编写密码保护功能的接受测试代码

现在是时候为我的密码保护功能添加测试了

首先,将我的原始测试“ spec”复制为伪代码:


@测试
公共 无效的 passwordProtect(){
// 1。 我创建一个新的白板
// 2。 我在上面设置了密码。
// 3。 乔试图打开我的白板,要求输入密码。
// 4。 Joe输入了错误的密码,并被拒绝访问
// 5。 Joe再次尝试,输入正确的密码并获得访问权限。
}

现在,我再次编写测试代码,并假装AcceptanceTestClient完全按照我的需要提供了我所需的一切。 我发现这项技术非常有用。


@测试
公共 无效的 passwordProtect(){
// 1。 我创建一个新的白板
myClient .createNewWhiteboard();
字符串whiteboardId = myClient .getCurrentWhiteboardId();

// 2。 我在上面设置了密码。
myClient .protectWhiteboard( “ bigsecret” );

// 3。 乔试图打开我的白板,要求输入密码。
尝试 {
joesClient .openWhiteboard(whiteboardId);
失败“ Expected WhiteboardProtectedException” );
} catch (WhiteboardProtectedException err){
//好
}
assertFalse(joesClient .hasWhiteboard());

// 4。 Joe输入了错误的密码,并被拒绝访问
尝试 {
joesClient .openProtectedWhiteboard(whiteboardId, “ wildguess” );
失败“ Expected WhiteboardProtectedException” );
} catch (WhiteboardProtectedException err){
//好
}
assertFalse(joesClient .hasWhiteboard());

// 5。 Joe再次尝试,输入正确的密码并获得访问权限。
joesClient .openProtectedWhiteboard(whiteboardId, “ bigsecret” );
assertTrue(joesClient .hasWhiteboard());
}

这段代码只花了几分钟的时间写完,因为我可以随身携带一些东西。 这些方法实际上几乎没有在AcceptanceTestClient中存在(尚未)。

在编写此代码时,我必须做出许多设计决策。 无需思考太多,只需要做想到的第一件事。 完美是足够好的敌人,现在我只想要足够好,这意味着可运行的测试失败了。 稍后,当测试运行并变为绿色时,我将进行重构并更加认真地考虑设计。

现在开始清理测试代码非常诱人,尤其是重构那些难看的try / catch语句。 但是,TDD的一部分纪律是开始重构之前变得绿色,测试将在重构时为您提供保护。 因此,我决定等待清理。

步骤3 –进行验收测试,但失败

跟随测试三角形,下一步是使其运行但失败。

同样,我使用Eclipse快捷方式让它为所有缺少的方法创建空版本。 非常好。 运行测试,瞧,我们有Red!

步骤4:将验收测试设为绿色

现在,我有很多生产代码要编写。 我在系统中添加了几个新概念。 当我这样做时,我添加的一些代码是不平凡的,因此需要进行单元测试。 我使用TDD做到这一点。 与ATDD相同,但规模较小。

这是ATDD和TDD结合在一起的方式。 将ATDD视为一个外部周期:

对于验收测试周期的每个循环(在功能级别),我们对单元测试周期进行多个循环(在类和方法级别)。

因此,尽管我的主要重点是将验收测试交给绿色(可能要花几个小时),但我的低层次的重点是例如将下一个单元测试委托给红色(通常只需要几分钟)。

这不是“皮革和鞭TDD”的硬核。 这更像是“至少确保单元测试和生产代码在同一提交中”。 每小时执行几次提交。 可能会称其为TDD-ish:o)

步骤5清理代码

像往常一样,一旦验收测试变成绿色,那就是清理时间。 永远不要跳过这一步! 这就像饭后洗碗–最快立即做。

我不仅清理生产代码,还清理测试代码。 例如,我将杂乱的try-catch内容提取到一个辅助方法中,并最终得到了这个漂亮干净的测试方法:


@测试
公共 无效的 passwordProtect(){
myClient .createNewWhiteboard();
字符串whiteboardId = myClient .getCurrentWhiteboardId();

myClient.protectWhiteboard( “ bigsecret” );

assertCantOpenWhiteboard( joesClient ,whiteboardId);

assertCantOpenWhiteboard( joesClient ,whiteboardId, “ wildguess” );

joesClient .openProtectedWhiteboard(whiteboardId, “ bigsecret” );
assertTrue(joesClient .hasWhiteboard());
}

我的目标是使验收测试简短,整洁,易于阅读,以免评论多余。 原始的伪代码/注释充当模板–“这是我希望这段代码清楚多了!”。 删除注释会带来胜利感,并且由于具有积极的副作用,因此使方法更短!

接下来是什么?

冲洗并重复。 一旦有了第一个测试用例,我就想到了缺少的东西。 例如,我说过密码保护应要求用户验证。 因此,我为此添加了一个测试,将其变为红色,使其变为绿色,然后进行清理。 等等。

这是我为此功能创建的测试的完整列表(到目前为止):

  • passwordProtectionRequiresAuthentication()
  • 保护白板
  • passwordOwnerDoesntHaveToKnowThePassword
  • 更改密码
  • removePassword
  • whiteboardPasswordCanOnlyBeChangedByThePersonWhoSetIt

我肯定会在以后发现错误或添加新功能时添加更多测试。

总而言之,这大约是有效编码的2天。 它的大部分内容是在代码和设计上进行回顾和重申,而不是像本文中看到的那样线性。

手动测试呢?

自动测试变成绿色之后,我也做了很多手动测试。 但是由于自动测试涵盖了基本功能和许多边缘情况,因此我可以将手动测试的重点放在更主观和探索性的东西上。 高级用户体验如何? 流程有意义吗? 可以理解吗? 我需要在哪里添加帮助文本? 设计在美学上可以接受吗? 我并不想赢得任何设计大奖,但是我也不想让任何丑陋的事情发生。

强大的自动验收测试套件消除了对无聊的重复手动测试(也称为“猴子测试”)的需求,并为更有趣和更有价值的手动测试类型腾出了时间。

理想情况下,我应该从一开始就构建自动化的验收测试,因此部分原因实际上就是我还清了一些技术债务。

关键要点

在那里,我希望这个例子对您有用! 它展示了一个非常典型的情况–“我将要构建一个新功能,编写一个自动验收测试会很好,但是到目前为止我还没有任何东西,我也不知道要使用什么框架或甚至如何开始”。

我真的很喜欢这种模式,它让我困扰了很多次。 综上所述:

  1. 假设您有一个很棒的框架封装在一个真正方便的帮助器类(在我的情况下为AcceptanceTestClient)的后面。
  2. 为今天已经可用的东西编写一个非常简单的验收测试(例如仅打开您的应用程序)。 使用它可以驱动您对AcceptanceTestClient的实现以及相关的测试配置(例如伪造与数据库和其他外部服务的连接)。
  3. 为您的新功能编写验收测试。 使它运行但失败。
  4. 使它变绿。 编码时,编写单元测试以查找任何非重要的内容。
  5. 重构。 也许编写更多的单元测试以取得良好的效果,或者删除多余的单元测试。 保持代码干净整洁!

完成此操作后,您已经越过了最困难的门槛。 您已经开始使用ATDD了!

翻译自: https://www.infoq.com/articles/atdd-from-the-trenches/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

从蓝海战略到长尾理论

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值