从蓝海战略到长尾理论
您是否曾经遇到过这种情况:
然后,本文为您服务-一个具体示例,说明如何开始在现有代码库上进行验收测试驱动的开发。 这是解决技术债务的一部分 。
这是一个包含疣和所有东西的真实示例,而不是精美的教科书示例。 所以穿上你的战靴。 我将只使用Java和Junit,而不会使用任何花哨的第三方测试框架(这些框架往往会被过度使用)。
免责声明:我并不是说这是正确的方法,还有ATDD的许多其他“风味”。 此外,本文中没有太多新的或创新的内容,这只是行之有效的实践和来之不易的经验。
几天前,我坐下来为webwhiteboard.com (我的宠物项目)构建了一个密码保护功能。 长期以来,人们一直在寻求一种用密码保护其在线白板的方法,因此是时候将其完成。
听起来像是一个简单的功能,但是要做出很多设计决策。 到目前为止,webwhiteboard.com一直基于匿名使用,并且没有任何类型的帐户,登录名或密码。 谁应该能够保护白板? 谁应该可以访问它? 如果忘记密码怎么办? 我们如何使事情简单但足够安全?
Webwhiteboard代码库具有不错的单元测试和集成测试范围。 但是它没有验收测试。 也就是说,从用户的角度来看,这些测试要经过端到端的流程。
Web白板的主要设计目标是简化:最大程度地减少对登录和帐户以及其他烦恼的需求。 因此,我为密码功能设置了两个设计约束:
这里有很多不确定性。 我不确定我希望它如何工作,甚至不确定我想要如何实现它。 所以这是我的方法(基本上是ATDD):
这是反复进行的,因此在每个步骤中,我都可以决定返回并调整上一个步骤(我经常这样做)。
假设功能已完成。 我睡着的时候一个天使来了并实施了它。 听起来好得令人难以置信! 我将如何验证? 我要手动测试的第一件事是什么? 这个:
当我编写这个小测试脚本时,我意识到要考虑很多替代流程。 但这是主要情况。 如果我能解决这个问题,那我就走了。
这是棘手的部分。 我没有其他的2端到2端验收测试,那么我该如何开始? 该功能将通过第三方认证系统(我的初步决定是使用Janrain)以及数据库进行交互,并有大量的参与弹出对话框和令牌和重定向和这种棘手的网页内容。 啊。
是时候退后一步了。 在解决“我如何编写此验收测试”问题之前,我需要解决一个更基本的问题“在此代码库中我该如何编写验收测试?”
为了提出这个问题,我试图确定我可以测试的“最简单的功能”,而今天该功能已经可以使用。
这是我想出的:
我将如何实施此测试? 哪些框架? 哪些工具? 它应该包含GUI还是绕开它? 它应该包含客户端代码还是直接与服务器对话?
很多问题。 诀窍是:不要回答他们! 只是假装一切都以某种方式精美地解决了,然后将测试编写为伪代码。 这里是。
公共 类 AcceptanceTest { @测试 公共 无效 openWhiteboardThatDoesntExist(){ // 1。 尝试打开一个不存在的白板 // 2。 检查我没有得到白板 } } |
我运行它,它成功了! 欢呼! 呃,别等,那是错的! TDD三角形中的第一步(“红绿色重构”)是Red 。 因此,我需要使其失败,以证明需要构建该功能。
我最好继续编写一些真实的测试代码。 但是,伪代码使我朝着正确的方向前进。
为了使该测试成为现实,我组成了一个名为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。
下一步是使验收测试变成绿色。
请注意,我现在要解决一个更简单的问题。 我不必担心身份验证以及多个用户和诸如此类的问题。 我可以稍后再添加测试。
至于AcceptanceTestClient,实现是非常标准的-模拟数据库(我已经有代码了)并运行整个Webwhiteboard系统的内存版本。
这是生产设置:
(点击图片放大)
技术细节:Web Whiteboard使用GWT(Google Web工具包)。 一切都是用Java编写的,但是GWT会自动将客户端代码转换为javascript,并插入RPC魔术(远程过程调用)以封装异步客户端-服务器通信的所有肮脏细节。
在验收测试设置,我“短路”系统并切断了所有的框架, 第三方服务和网络通信。
(点击图片放大)
因此,我创建了一个AcceptanceTest客户端,该客户端以与真实客户端相同的方式与Web白板服务对话。 区别在于窗帘。
同样,在验收测试配置中,它将用伪造的内存数据库替换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?”。 有两个原因:
我不会为您带来更多关于AcceptanceTestClient代码的细节,因为本文不是关于Web白板的内部管道。 可以说,AcceptanceTestClient将接受测试的需求映射到与白板服务接口进行交互的底层细节。 这很容易实现,因为真实的客户端代码可以有效地充当“如何与服务交互”教程。
无论如何,现在我们最简单的验收测试通过了!
@测试 公共 无效 openWhiteboardThatDoesntExist(){ myClient .openWhiteboard( “ xyz” ); assertFalse(myClient .hasWhiteboard()); } |
下一步是清理一下。
实际上,我没有为此编写任何生产代码(因为该功能已经存在并且可以正常使用),它只是测试框架代码。 但是,尽管如此,我还是花了几分钟的时间清理,删除重复项,使方法名称更清晰等。
最后,出于完整性考虑,我又添加了一个测试,因为它很简单:o)
@测试 公共 无效 createNewWhiteboard(){ 客户端 .createNewWhiteboard(); assertTrue( 客户端 .hasWhiteboard()); } |
欢呼,我们有一个测试框架! 而且,我们甚至不需要任何精美的第三方库。 只是Java和Junit。
现在是时候为我的密码保护功能添加测试了
首先,将我的原始测试“ 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的一部分纪律是在开始重构之前变得绿色,测试将在重构时为您提供保护。 因此,我决定等待清理。
跟随测试三角形,下一步是使其运行但失败。
同样,我使用Eclipse快捷方式让它为所有缺少的方法创建空版本。 非常好。 运行测试,瞧,我们有Red!
现在,我有很多生产代码要编写。 我在系统中添加了几个新概念。 当我这样做时,我添加的一些代码是不平凡的,因此需要进行单元测试。 我使用TDD做到这一点。 与ATDD相同,但规模较小。
这是ATDD和TDD结合在一起的方式。 将ATDD视为一个外部周期:
对于验收测试周期的每个循环(在功能级别),我们对单元测试周期进行多个循环(在类和方法级别)。
因此,尽管我的主要重点是将验收测试交给绿色(可能要花几个小时),但我的低层次的重点是例如将下一个单元测试委托给红色(通常只需要几分钟)。
这不是“皮革和鞭TDD”的硬核。 这更像是“至少确保单元测试和生产代码在同一提交中”。 每小时执行几次提交。 可能会称其为TDD-ish:o)
像往常一样,一旦验收测试变成绿色,那就是清理时间。 永远不要跳过这一步! 这就像饭后洗碗–最快立即做。
我不仅清理生产代码,还清理测试代码。 例如,我将杂乱的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()); } |
我的目标是使验收测试简短,整洁,易于阅读,以免评论多余。 原始的伪代码/注释充当模板–“这是我希望这段代码清楚多了!”。 删除注释会带来胜利感,并且由于具有积极的副作用,因此使方法更短!
冲洗并重复。 一旦有了第一个测试用例,我就想到了缺少的东西。 例如,我说过密码保护应要求用户验证。 因此,我为此添加了一个测试,将其变为红色,使其变为绿色,然后进行清理。 等等。
这是我为此功能创建的测试的完整列表(到目前为止):
我肯定会在以后发现错误或添加新功能时添加更多测试。
总而言之,这大约是有效编码的2天。 它的大部分内容是在代码和设计上进行回顾和重申,而不是像本文中看到的那样线性。
自动测试变成绿色之后,我也做了很多手动测试。 但是由于自动测试涵盖了基本功能和许多边缘情况,因此我可以将手动测试的重点放在更主观和探索性的东西上。 高级用户体验如何? 流程有意义吗? 可以理解吗? 我需要在哪里添加帮助文本? 设计在美学上可以接受吗? 我并不想赢得任何设计大奖,但是我也不想让任何丑陋的事情发生。
强大的自动验收测试套件消除了对无聊的重复手动测试(也称为“猴子测试”)的需求,并为更有趣和更有价值的手动测试类型腾出了时间。
理想情况下,我应该从一开始就构建自动化的验收测试,因此部分原因实际上就是我还清了一些技术债务。
在那里,我希望这个例子对您有用! 它展示了一个非常典型的情况–“我将要构建一个新功能,编写一个自动验收测试会很好,但是到目前为止我还没有任何东西,我也不知道要使用什么框架或甚至如何开始”。
我真的很喜欢这种模式,它让我困扰了很多次。 综上所述:
完成此操作后,您已经越过了最困难的门槛。 您已经开始使用ATDD了!
翻译自: https://www.infoq.com/articles/atdd-from-the-trenches/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1
从蓝海战略到长尾理论