快乐学习
前程无忧、中华英才非你莫属!

Cucumber-java版真正的入门到精通(Day6)

在前一章中,我们开始通过一个例子来说明如何使用Cucumber在外部构建真正的应用程序。我们正在建立的系统是银行的自动柜员机(ATM),我们使用Cucumber来帮助我们设计一个简单的域模型来满足这种情况:
在前一章中,我们开始通过一个例子来说明如何使用Cucumber在外部构建真正的应用程序。我们正在建立的系统是银行的自动柜员机(ATM),我们使用Cucumber来帮助我们设计一个简单的域模型来满足这种情况:
修复Bug      
有时候,你从代码中得到的信号是错误的,很微弱。我们总是尽量保持待办事项列表的方便,写下我们所关心的所有事情。这样一来,我们就可以把精力集中在手边的任务上,但是知道在下一个任务之前,我们将有机会检查和清理任何不一致的地方。                当我们完成最后一个场景时,我们注意到了Teller.withdraw_from()有两个参数,但是在方法中只使用了其中的一个参数来使场景通过。这种不一致意味着什么是错误的,但我们需要进一步调查以了解我们需要做些什么。我们来看看使用该方法的代码:
这似乎很明显,我们打算在这里发生。该柜员应采取指定金额的钱出来的帐户,并把它交给了CashSlot。那么,当我们实现这个方法的时候,我们是如何设法让这个场景通过而不需要对账户进行任何操作的呢?让我们再看看这个场景:
A-HA!这里没有任何东西可以检查账户余额是否已经减少到80美元。我们现在抓住这件事是一件好事 - 我们的代码将免费向客户发放现金!
马特说:这不是一个错误; 这只是一个失踪的情景                        
当我第一次使用Cucumber从外部构建一个完整的应用程序时,我发现了一件奇妙的事情,那就是我们开始手动探索性测试并发现了一些错误。几乎没有例外,这些错误中的每一个都可以追溯到我们为系统的这个部分编写的黄瓜场景中的一个缺口。因为我们共同编写了这些场景,与商人和开发人员共同努力,以确保它们正确无误,没有任何人有错误。这只是我们原本预料不到的一个边缘案例。                              
根据我的经验,错误是软件团队中摩擦和不快的重要原因。商人们把他们归咎于不小心的开发人员,而开发人员则指责他们对商人的要求不够。事实上,他们只是软件开发的一个自然结果,这是一项复杂的工作。使用黄瓜帮助球队看到缩小这些差距是每个人的工作。责备只是没有发生,结果我们是一个更快乐的团队。            
让我们钉在这额外的结果作为另一个然后步骤:
运行mvn clean test并将新步骤定义的代码粘贴到文件AccountSteps.java中:
我们可以使用前一章中创建的变换编写新的片段,并复制上一步定义中的断言:
们希望继续并修复这个bug,所以我们会在我们的待办事项清单上做一个记录,以清理我们刚刚介绍的重复内容。现在运行mvn clean test来查看是否有困难:
 而我的帐户余额应该是$ 80.00
太好了,这个错误已经被我们的情况所吸引。正如我们所怀疑的那样,账户的余额仍然没有被撤回。但不长久!打开Teller.java,看看我们的Teller类:
现在当我们运行mvn clean test时,编译失败,告诉我们需要在Account上创建这个新的借记方法。在一个真实的项目中,我们会在这个时候下降一个档次,并开始为Account类写单元测试来驱除这个方法。我们不介意使用一些基本的方法存根绘制接口,只有Cucumber支持,但是一旦我们开始向类中添加有趣的行为,我们希望确保行为已经在一个单元测试。          chelimsky缺少参考书目:rspec和面向对象的软件,由测试引导  [FP09] 都用大量的例子来解释这个平衡,但是由于这本书是关于黄瓜的,所以我们将会把这个步骤做一下,继续下去。           
将借项添加到Account类:
我们实施了一种称为借记的新方法,将余额递减给定金额。让我们运行mvn clean test,看看我们是否设法修复了这个bug:
万岁,我们做到了!现在是我们继续前进行一些重构的时候了。    
审查和重构                                        
在极限编程解释  [Bec00]中,Kent Beck给出了一个简单设计的四个标准。为了最重要的第一,他们如下:              
1、通过所有的测试
2、揭示了所有的意图
3、不包含重复
4、使用最少数量的类或方法        
让我们来看看我们当前的代码对这四个标准。这是通过一个单一的黄瓜场景的测试,通过了,所以这是规则1照顾。规则2呢?揭示意图主要是关于事物的命名方式,这对我们来说很重要。让我们来看看我们的Account类中的方法:
反思它,我们使用两个方法名称存款和借记令人困惑。真的,我们希望看到存款和提款或贷记和借记,但不是两者的结合。哪一对更适合我们的帐户类?                                      
我们花了一点时间与我们的一位域名专家聊天,很明显,信用卡和借记卡是帐户方法的正确名称。事实上,存款是你更可能要求出纳员为你做的。当你开始建立一个无所不在的语言时,这些对话就会一直发生,你会发现它们变得越来越容易和容易,因为你对领域的知识以及所有用来讨论它的无处不在的语言都在增长。              我们将把Account.deposit方法重命名为Account.credit。由于我们的测试很好,而且速度很快,我们可以重命名该方法并查看什么是中断。(当然,你可以在你最喜欢的IDE中调用Rename重构,但是我们想要展示如何用一个文本编辑器和一个编译器来做到这一点):
在重构:改进现有代码的设计  [FBBO99]中,这种技术(做出改变,看看有什么断点)被称为靠在编译器上。从编译失败看来,我们需要更改AccountSteps.java的第26行。继续并改变它,再次运行mvn clean test。                     
 我们的情况已经过去了,但我们还没有完成!现在步骤中的语言与我们刚刚更改的步骤定义中的代码不一致:
测试自动化是软件开发                    
你会注意到我们在功能,步骤定义和系统本身之间来回移动的细心的关心,保持一切尽可能的干净和一致。你可能想知道我们在日常工作中是否诚实地以这种方式工作。我们当然试图这样做。本书前面已经提到了这一点,但是我们觉得现在是时候明确表达了:如果你正在编写自动化测试,那么你正在开发软件。如果你重视这些测试,足以把它们写在第一位,那么你将希望能够在未来回来改变它们。这意味着我们通常用来编写可维护软件的所有相同的良好习惯也适用于我们编写的测试代码。                
这个重要的点经常被忽略,特别是在一个传统上手动测试的公司中。专门从事测试的人有时并不是自动化测试的最佳人选,如果他们缺乏必要的软件设计经验的话。没有了解如何编写可维护代码的人的支持,团队可能会遇到难以改变的混乱的测试代码。人们开始意识到自己的自动化测试实际上使得软件更换变得更加困难,而不是简单,并且考虑放弃黄瓜。下图显示了软件开发技能在从Gherkin功能下移到支持代码时变得越来越重要。
测试和软件设计是互补的技能,一个强大的团队需要混合使用这两种专业。不同的人会坐在这个范围的不同点上:有些人会是伟大的测试人员,但对自动化或编程不感兴趣,有些人只是想编写代码而不考虑如何打破它。这很好,但是团队需要认识到,在进行自动化测试时,并没有明确的职责分工。每个人都需要共同努力来创建和维护高质量的测试。
在这一步中,我们仍然使用“ 存入 ”一词,但是现在我们在下面的Java代码中记入这个帐户。这有多重要?也许并不多,但是我们希望我们的测试代码尽可能透明。经过与我们的领域专家的另一个讨论,我们决定改写步骤阅读鉴于我的帐户已记入$ 100.00。我们将更改该功能和基础步骤定义:
现在看这个步骤的定义,检查余额是否与记入金额相同是没有意义的。如果这个步骤的定义是在另一个涉及其他信用卡或借记卡交易的情况下使用的,那么余额可能与记入的金额不一样,那就没什么问题了。所以,我们不希望这里再有这个断言。              这给了我们一个简单的方法来将我们的待办事项清单上的最后一个项目删除:在帐户的两步定义中删除重复的平衡声明。我们可以简单地删除第一个,因为它不再相关:
这完成了我们的重构。现在的代码如同我们现在所能想象的那样清晰和交流,没有重复。我们准备添加一些新的功能。

使用钩子            
黄瓜支持钩子,这是在每个场景之前或之后运行的方法。您可以使用注释@Before和@After在支持或步骤定义图层中的任意位置定义它们。          
为了测试它们,添加一个如下所示的文件src / test / java / hooks / SomeTestHooks.java:
如果运行mvn clean test,则会在方案的开始和结尾处看到在输出中显示的两条消息。Cucumber的@Before和@After钩子很像XUnit系列测试工具中的SetUp和TearDown方法。    
@Before和@After挂钩      最常见的用法是清除外部系统(如数据库)中留下的任何残留状态,以便每个场景都以系统处于已知状态的方式启动。我们将在第10章详细,说明这个数据库。          
钩子默认是全局的,这意味着它们针对你的特性中的每个场景运行。如果您希望它们仅运行某些场景,则需要标记这些场景,然后使用标记的挂钩。    
标记钩                        
无论@Before和@After接受标记的表达,你可以用它来选择挂钩加起来也只有某些情况下。例如,假设您正在为网站的管理区域编写功能。每个管理员功能都从以下背景开始:
另一种方法是标记功能,然后使用@Before钩子运行相同的代码以管理员身份登录。
现在,要运行以管理员身份登录的方案,只需使用@admin标记该方案,并且此代码将在方案的步骤之前自动运行。在筛选与标记表达式,我们将使用逻辑运算解释更为复杂的标记表达式,但一个简单的标签会为现在要做的。                      
标记的钩子可以用于确保像外部服务开始这样的技术性事物,而不会在场景本身的文本中对它们造成太大的惊扰。
Aslak说:步骤定义是全球性的                    
当你定义一个步骤定义时,它是全局定义的。没有办法将步骤定义的范围缩小到某些情况下,就像您可以使用标记的挂钩一样。                  人们偶尔会要求以类似的方式来标记步骤定义的方法,例如当我关闭它时,为某些情景调用一个步骤定义,为另一个情景调用另一个步骤定义。                  
这是一个很容易添加到Cucumber的功能,有一天,我实际上已经实现了这个功能,从Cucus邮件列表上的好人那里得到一些反馈。我的问题是,任何人都可以想到人们如何滥用这个?名单上的老前辈理查德·劳伦斯(Richard Lawrence)回答说:                                                          功能耦合的步骤是极端的。更微妙的问题是,当变得太容易说“哦,这只是另一个上下文,我会用相同的词来表示不同的东西”,那么发展无处不在的语言的有益压力就会消失。我有教练团队学习无处不在的语言的对话,我希望这会发生很多。                            “功能耦合步骤”这个术语是我在黄瓜早期提出的一个术语,当时我正在记录维基上黄瓜的好坏。我认为功能耦合的步骤是一种代码异味,因为它们很快导致大量的重复,并且无法促进无处不在的语言。                  
当Dan North(BDD的创始人)编写了他的第一个BDD框架时,步骤定义与特征相结合。他告诉我,跨功能共享全局步骤定义的能力是Cucumber带来的改进之一。                  回
想起来,观察我们故意摆脱的黄瓜重新引入机制的克隆和衍生是相当有趣的。                  
在黄瓜里,同一句话只能意味着一件事。
检查情景        
如果我们想要的话,我们的钩子可以接受代表场景的单个参数。例如,我们可以问一个情景:      
有关Scenario对象的更多详细信息,请参阅cucumber.api.Scenario的文档。[39]
有关Scenario对象的更多详细信息,请参阅cucumber.api.Scenario的文档。[39]
钩在其他时间运行        
如果要在所有功能开始之前运行代码,那么要使用Java语言功能(如静态字段)。                               
如果要在所有功能完成后运行代码,则可以使用Java的内置addShutdownHook,它将在Cucumber进程退出之前运行。您通常会使用此机制来拆除您从支持代码开始的一些外部系统。                      
有时,人们也会询问关于特定功能运行特定的设置和清理代码,而不是针对功能中的每个场景。有很多方法可以实现这一点,而不需要Cucumber来提供任何附加功能,Paolo Ambrosio详细介绍了其中的一个功能。[40]            
有了这些关于钩子的新知识,让我们回到我们的ATM上工作。
乔问:我的钩子运行的顺序是什么?                
有时,能够指定钩子运行的确切顺序非常重要。@Before和@After注释具有您可以设置的顺序参数。对于没有特定订单集的任何挂钩,默认值为10000。
黄瓜运行@Before挂钩从低到高。甲@Before与钩顺序的10将一个之前运行与顺序的20 @After在相反的运行方向钩-从高至低的这样@After与钩顺序 20的将一个之前运行与顺序 10。              
如果您需要在标记的钩子上使用顺序,则必须使用value参数:

我们新的异步架构                        
在真实的银行系统中,自动取款机并不是唯一的账户借方。您可以在餐厅或超市使用借记卡,也可以走进银行,在柜台上取款。你可能写了一张支票,几天后兑现。您还可以在您的账户中存入信用额度,例如在银行存款时。银行将这些事件视为一个交易,在事件发生后的一段时间内处理。请注意,这种交易与数据库交易完全不同,我们将在下一章讨论这个问题。          
我们将改变我们的架构,以便当客户从ATM取款时,它将关于借记交易的消息发布到消息队列中。我们将把处理这个队列的责任转移到一个单独的后端服务中。由于后端服务通过交易队列工作,它将更新的账户余额存储在数据库中,以便ATM(以及我们的测试)可以访问它。          

下图显示了新架构的外观。     


图3.企业消息架构        

该ATM张贴关于交易到消息中事务队列。该交易处理器读取消息关闭该队列,读取从现有的平衡平衡店,然后把更新的余额回到平衡商店。该ATM读取从账户余额余额商店。         

 我们来看看这个新体系结构对我们如何测试系统的影响。


如何同步                        
在我们目前的实施中,我们简单的Account类同步处理借记和贷记交易:
该实施中,在平衡更新过程的方法调用信用卡或借记卡,意味着我们可以肯定的余额已被时间更新黄瓜检查最终账户余额那么我们的场景中的步骤。
但是,当我们转换到新架构时,交易将由一个单独的后端服务来处理。由于测试和后端服务在不同的进程中运行,因此在事务处理器完成工作之前,Cucumber很可能会运行Then步骤。如果发生了这种情况,即使系统实际上按预期工作,测试也会失败:如果只有黄瓜等了一会儿,就会看到正确的平衡。这就是我们所说的一个闪烁的情况下,如第6章所描述的,使你的黄瓜甜。我们如何判断何时可以安全地查看帐户余额?          
将异步组件添加到系统中引入了一定程度的随机性,但对于我们的测试是可靠的,我们需要确保行为是完全确定的。要做到这一点,我们需要了解如何使我们的测试与系统同步,以便我们只有在系统准备就绪时才进行检查。在越来越多的面向对象软件中,通过测试  [FP09],Steve Freeman和Nat Pryce确定了两种同步异步系统测试的方法:采样和监听。                
通过侦听进行同步                

监听事件是同步异步系统的最快和最可靠的方法。为了使这种技术发挥作用,被测试的系统必须被设计成当某些事情发生时触发事件。测试订阅这些事件,并可以使用它们在场景中创建同步点。              例如,如果事务处理器正在将BalanceUpdated事件发布到发布 - 订阅消息通道中,则可以在我们的Then步骤的顶部等待,直到听到该事件。一旦我们收到这个事件,我们就知道继续进行并检查余额是安全的。如果系统出现问题,我们将使用超时确保测试不会永远等待。              

使用这样的事件需要对测试和开发工作进行复杂的协调,但是这会导致快速测试,因为它们不会浪费任何时间等待系统:只要他们得到正确的事件通知,就会回到行动和继续。              

通过采样进行同步                

当不能从系统收听事件时,下一个最好的选择是反复轮询系统,查看你所期望的状态变化。如果在某个超时时间内没有出现,则放弃测试并通过测试。                      

由于轮询间隔的原因,采样可能导致测试比收听慢一点。对系统进行更改的次数越多,系统按照预期工作时,测试可以更快地反应并执行。但是,如果您频繁轮询,则可能会使系统负担过重。             

如果您无法从被测系统接收事件,采样通常是实用的选择。在本章的其余部分中,我们将向您展示如何使用采样来使我们的测试在新架构下可靠运行。
实施新的架构      
因此,我们可以告诉你什么是闪烁的情况,我们将开始通过更改相同的测试代码下的体系结构。我们将演示闪烁的场景,并在最后的繁荣中,通过更改测试以使用采样与系统同步来修复它。    
赶出接口       
该账户类是在ATM会与我们的新的后端服务的接口。如图3中所示,企业消息架构,两个触摸点是TransactionQueue和BalanceStore。让我们通过改变我们的Account类来使用两个可以与这些服务对话的假想对象来设计这些对象的接口:
取得平衡是委托给BalanceStore的一个简单问题。在一个更现实的系统中,我们需要告诉BalanceStore哪个帐户是我们想要的余额,但是在我们的简单示例中,我们只处理一个帐户,所以我们不必担心这个问题。              
对于借方和贷方,我们将交易序列化为一个字符串,使用+或-来指示金额是信用还是借方,然后将其写入队列。      
构建TransactionQueue                        
让我们建立我们的事务队列。我们希望保持这个例子的技术简单,所以我们将使用文件系统作为我们的消息存储,每个消息作为一个文件存储在消息目录中。当从队列中读取消息时,我们将删除该文件。代码如下:
这是相当简单的Java代码,但这是迄今为止我们在本书中最复杂的一个,所以我们来看看它是如何工作的。首先我们有一个静态方法,TransactionQueue.clear从第15行开始,我们将使用它来确保队列在场景之间清理。当我们初始化TransactionQueue时,我们在第13行创建了一个实例变量nextId,它将用来给每个新消息一个唯一的文件名。当我们被要求写一条消息(第24行)时,我们在消息目录(第30 行)中创建一个新文件,将消息的内容写入文件(第37行),然后递增nextId(第40行)准备好命名下一个消息的文件。           
当我们被要求阅读一条消息时(第43行),我们得到了消息目录中所有文件的列表(第46行)。如果目录是空的,我们只是从方法中返回一个空的消息(第81行)。如果目录不为空,那么我们通过消息ID对消息列表进行排序(第52行)。我们打开第一条消息(第63行),读取消息(第66行),从队列中删除消息(第70行),并将内容返回给调用者(第81行)。              
构建BalanceStore       
该BalanceStore是其中最新的帐户余额进行存储的数据库。再一次,我们想要保持这个例子的技术简单,所以我们将使用一个非常简单的数据库:磁盘上的文本文件。代码如下:
我们有两种方法,一种是读取天平(第18行),另一种是设置天平(第33行)。他们都在我们项目的根目录中使用一个名为balance的文件(第10行)。当询问余额时,BalanceStore打开余额文件(第19行),读取内容(第22行),并将它们转换为数字(第27行)。当要求设置余额时,BalanceStore打开余额文件(第37行)并将新余额写入(第43行)。简单!             
现在我们在TransactionQueue和BalanceStore中保持状态到磁盘,我们需要小心,我们不会泄漏任何状态。尽管目前我们的功能只有一个场景,但我们需要在每次运行时进行清理,以便余额和消息不会从一次测试运行中泄漏到下一次。      
添加挂钩重置状态                
在我们的场景运行之前,我们有两个地方需要清理状态。我们需要将用户的帐户余额设置为零,并且我们需要删除已经留在事务队列中的所有消息。用下面的代码添加一个src / test / java / hooks / ResetHooks.java文件:
我们直接创建了BalanceStore的新实例,以便我们可以告诉它将余额设置为零。然后我们使用前面创建的TransactionQueue.clear方法清空事务队列中的所有消息。                      
通过编写我们的TransactionProcessor,让我们把最后一块放在难题中。      

构建TransactionProcessor                
该TransactionProcessor是一些代码,会在后台运行,深藏在我们的银行的服务器机房的肠子。代码如下:
事务处理器首先创建一个TransactionQueue和BalanceStore类的实例。一旦启动,它就进入一个循环,试图从事务队列中读取消息。如果找到一个,它暂停一秒钟,计算新的余额,然后将其存储在BalanceStore上。我们介绍了暂停来演示在我们的系统中使用异步组件的效果。这个延迟应该意味着测试将会一直失败,因为后端需要很长时间来更新黄瓜已经完成的情况。              

用依赖注入简化设计        
在依赖注入,我们看到了黄瓜如何使用依赖注入(DI)共享的实例KnowsTheDomain我们一步定义之间,但我们真的只是皮毛而已。现在是时候深入一点。       
在本章中,我们将讨论DI如何帮助改进测试代码的设计以及与Cucumber集成的各种DI容器。然后,我们将深入并重构我们的ATM示例以更有效地使用DI,向您展示如何使用四个流行的DI容器来实现。
DI和黄瓜      
你并不需要在使用黄瓜使用DI容器。当你使用没有DI集成的Cucumber时,它自己管理你所有的钩子和步骤定义的创建。Cucumber为每个场景创建每个步骤定义或钩子类的新实例。当然这意味着这些类需要有一个默认的构造函数; 否则黄瓜将不知道如何创建它们。这使得难以在几个步骤定义类之间安全地共享状态。          
DI可以使你的一些日常工作变得单调乏味,容易出错。只要添加其中一个DI集成,DI容器就会负责创建钩子和步骤定义。所有DI容器也可以在创建它们时将对象注入到我们的钩子和步骤定义中。更好的是,DI容器将创建任何需要创建的对象的实例,以便您可以轻松地构建依赖对象的网络,而将所有对象连接到DI容器上的工作量很大。                     
DI容器注入对象的两种常见方式是构造函数注入和字段注入。在下面的章节中,我们将主要使用构造函数注入,但是我们也将向您展示实际注入的行为。    
让DI管理共享状态                        
DI容器只是一个为我们创建和管理一些实例的工具。如果您回头看看我们编写的代码,在所有的步骤定义类中共享一个KnowsTheDomain实例,您将看到我们从来没有使用new创建一个KnowsTheDomain 实例。那是因为我们的DI容器PicoContainer一直在为我们做。更重要的是,PicoContainer创建了一个KnowsTheDomain的新实例对于每个场景,并将该实例注入每个需要它的步骤定义类中。这使得我们可以轻松地为我们的应用程序中的每个域实体创建一个关键的步骤定义类,依靠PicoContainer来共享它们之间的状态。                     
如果我们在没有DI的情况下这样做,那对我们来说意味着更多的工作。我们可以通过创建一个KnowsTheDomain的静态实例来共享状态,但是这个实例会被我们所有的场景共享。由于我们希望每个场景都有自己的KnowsTheDomain的新副本,所以我们必须添加@Before挂钩来重置共享实例。但是我们不需要这样做,因为DI容器会为我们做。              
黄瓜使用DI使我们的生活变得更简单,通过创建我们的钩子和步骤定义类以及所依赖的所有共享状态。对于需要访问场景共享状态的每个步骤定义,我们定义一个将共享类作为参数的构造函数。如果一个场景需要访问几个不同类的实例,我们只需提供一个构造函数,它为每个类提供一个参数:

你会想在你的大多数Cucumber项目中使用DI,因为它使步骤定义类之间的共享状态变得更加简单。黄瓜与几个DI容器集成,我们将在下面简要介绍一下。             
DI容器集成                
黄瓜运送与几个更受欢迎的DI容器集成(以及一些大多数人不熟悉),如下表所示。您所写的代码将根据您选择的DI容器而略有不同。                         
黄瓜-picocontainer -PicoContainer:[49] AslakHellesøy,Paul Hammant和Jon Tirsen                                  
cucumber-guice -Guice:[50]来自Google的轻量级DI容器                                  
黄瓜spring:[51]一个流行的框架,包括DI和更多                                  
cucumber-weld- CDI / Weld:[52] CDI的参考实现(用于Java EE平台的上下文和依赖注入框架)                                             
cucumber-openejb -OpenEJB:[53]来自Apache的独立EJB服务器,包括CDI实现通过在你的类路径中包含相关的Cucumber JAR来选择使用哪个框架,但只有一个 Cucumber DI JAR应该在类路径中。只要将其中一个JAR放在类路径中,Cucumber就会将您的钩子和步骤定义的创建和管理委托给您选择的DI容器。黄瓜JAR只包含集成DI容器的代码 - 您还需要添加DI容器本身的依赖关系。      我应该选择哪个DI容器?          各种DI容器提供几乎完全相同的功能。每个需要稍微不同的配置,但是选择主要取决于您的应用程序中已经使用的DI容器。如果你的应用程序使用Spring,那么选择黄瓜春天。如果你的应用程序使用Guice,那么选择cucumber-guice。                  如果你的应用根本不使用DI,PicoContainer是一个不错的选择,因为它使用起来非常简单。                

让我们回到我们的ATM示例,看看DI如何改进测试代码的结构。你会惊讶它可以做出什么大不了的。

使用DI改进我们的设计                  

正如我们所看到的,通过为我们做一些工作,DI可以使我们的生活变得更简单。目前我们的应用程序正在使用DI来共享一些状态,但是在KnowsTheDomain中的代码正在管理着不少域实体的创建。随着应用程序的增长,我们可能会发现更多需要在步骤定义之间共享的域实体。诱惑将是把我们所有的共享领域实体放入KnowsTheDomain,但是这将很快成长,展示怪物对象反模式。[54]           为了保持步骤定义的可维护性,为每个域实体创建一个步骤定义类是一个好主意。很明显,现金槽步骤定义将需要与现金槽域实体交互,但它是否需要知道关于客户实体?可能不会,那么我们为什么要把它交给一个可以访问客户的帮手呢?                        
从第10章的结束代码开始,数据库,我们要重构我们的ATM例子更有效地使用DI。我们将采取小步骤,[55]看每个重构的结果,看看是否有另一个重构,可以进一步改善设计。最后,你会看到一个更清洁的应用程序,更少的类和更清晰的架构。     分解KnowsTheDomain                
我们首先将KnowsTheDomain分成几个更小的粘性帮助类(每个域实体一个),如图所示。然后我们就可以确保一个步骤定义只能访问它的实体应该需要通过将它们在施工时间进行交互。
分解KnowsTheDomain                
我们首先将KnowsTheDomain分成几个更小的粘性帮助类(每个域实体一个),如图所示。然后我们就可以确保一个步骤定义只能访问它的实体应该需要通过将它们在施工时间进行交互。
我们将把特定于每个域实体的功能移到一个单独的帮助类中。看看KnowsTheCashSlot,例如:
同样,我们可以创建KnowsTheTeller和KnowsTheAccount帮助类。                 
接下来,我们必须更改对KnowsTheDomain的引用,例如CashSlotSteps:
并非所有的变化都非常简单。例如,我们的TellerSteps与KnowsTheAccount以及KnowsTheTeller交互,所以我们必须将两者都传递给构造函数。DI框架可以处理多个参数,并正确调用构造函数,而不需要我们做任何额外的工作:
一旦我们完成将功能移动到新的帮助类中,我们注意到在KnowsTheDomain中还有一些代码与创建共享的EventFiringWebDriver有关。由于这是特定于驱动用户界面的技术,所以在逻辑上不是任何KnowsTheXxx类的问题,所以我们需要决定将它放在哪里,接下来我们要做什么。
提取Web驱动程序      

既AtmUserInterface和WebDriverHooks依赖于共享EventFiringWebDriver。这曾经由KnowsTheDomain管理,但真的与域无关。相反,我们来提取一个新的MyWebDriver类:



KnowsTheDomain中   
没有代码,所以我们可以删除它。然而,我们仍然需要通过改变它们的构造函数把共享的MyWebDriver实例注入到AtmUserInterface和WebDriverHooks中:


(WebDriverHooks构造函数看起来一样。)        
现在我们运行mvn clean test来确保我们没有引入任何缺陷。              
我们通过将UI与域实体分离来改进了我们的设计。我们的DI容器现在负责管理MyWebDriver的共享实例,并将其注入到任何需要它的构造函数中。下一步,我们将看到大多数的帮助类没有做任何事情(或者很少),我们的DI容器不能为我们做,所以我们将它们完全重构。              
替换助手类                
在这一点上,我们创建了更小,更有凝聚力的帮助类,现在让我们仔细看看每个帮助类,看看我们是否满意测试代码的结构。现在我们有更小的帮助类,更容易看到每个人做什么和决定什么,武装我们对DI的更深入的理解,如果我们能做得更好。如果我们认为我们可以进一步改进,我们仍然有一个安全的过去的情况,以确保我们在继续重构时不会破坏任何东西。      
KnowsTheCashSlot          
看一下KnowsTheCashSlot类,我们可以看到,它所做的一切都是管理域实体CashSlot的创建。我们首先使用DI的原因是管理共享对象的创建,所以看起来很奇怪,我们已经结束了我们自己的类来做到这一点!发生的事情是,通过重构KnowsTheDomain,以及我们对DI如何工作的新知识,现在我们可以看到,我们根本不需要KnowsTheCashSlot。因此,我们通过删除KnowsTheCashSlot并直接将CashSlot注入到我们的步骤定义中来简化我们的代码库:
我们还需要将CashSlot注入到我们的ServerHooks中:
运行mvn clean test来确保我们仍然是绿色的!
KnowsTheTeller         
 KnowsTheTeller也只管理出纳员的创建。如果我们进行完全相同的更改以删除类,则尝试创建TellerSteps时会遇到运行时错误。这是因为DI容器不知道要实例化Teller接口的哪个实现。我们通过改变TellerSteps构造函数的签名来告诉它要实例化哪个具体类:
再次运行mvn clean test。
KnowsTheAccount          
清除知道账户比较复杂,因为(现在清楚)它有几个责任。
它执行以下操作:                      
1、打开数据库连接(如有必要)                        
2、 删除任何现有的帐户                         
3、 创建一个具有指定帐号的帐号                      
让我们开始将数据库连接和帐户删除移动到ResetHooks。我们需要确保这一点,需要一个数据库连接任何其他挂钩之前运行,所以我们指定一个低的顺序号:
现在KnowsTheAccount唯一的责任就是在数据库中存储一个测试账户,所以名字不再有意义。重命名TestAccount并让它扩展 帐号:


现在,当我们直接将TestAccount注入到我们的步骤中时,PicoContainer将确保它们都获得对同一个新创建的Account对象的引用。试试吧,它仍然可以正常工作。              在本节中,我们可以完全不使用DI,但是当DI容器负责为我们管理共享实体的创建时,可以看到事情变得如此简单。这有点像JVM中的垃圾回收方式,让我们不必过于担心创建和删除对象实例。          到目前为止,我们一直在使用PicoContainer作为我们的DI容器。接下来,我们来看看我们如何将它与我们的应用程序整合在一起。

PicoContainer几乎看不见            
PicoContainer可能是JVM上最简单的DI容器,这就是我们使用它的原因。这是最简单的与黄瓜融合。其他一些容器有更多的选择,但PicoContainer对于大多数应用程序是足够的。不使用PicoContainer的最可能原因是您的应用程序已经在使用另一个DI容器。                      
我们使用PicoContainer是如此的不显眼,以至于您现在可能已经忘记了所有这一切。我们甚至使用它的唯一证据是在我们的pom.xml中的两个依赖:
这个依赖将cucumber-picocontainer JAR放在类路径上,它告诉Cucumber让PicoContainer处理钩子和步骤定义的创建。
这第二个依赖项将PicoContainer实现 JAR放在类路径上。如果没有这个黄瓜将无法找到PicoContainer,将无法委托给它。         
 除了添加这些依赖项之外,您可以使用PicoContainer而不添加任何代码或注释。在下面的章节中,我们将把这个例子移植到其他的DI容器中,我们将会看到PicoContainer是唯一一个需要我们这么少配置的应用程序。

Guice是来自Google的DI容器,具有许多与PicoContainer相同的功能。
在我们看来,使用PicoContainer并不是那么容易,但它确实有一些额外的可能性,我们将会看到。另外,由于它是流行的Google工具集的一部分,因此您可能已经熟悉它了。          
我们将很快修改现有的ATM解决方案,与Guice一起运行,指出我们的差异。进一步的细节可以在Guice网站上找到。[56]需要注意的是在撰写本文时吉斯4.0是处于测试阶段,但本例使用吉斯3.0和黄瓜吉斯-1.2.0。          
切换DI容器        
我们将从上一节结束时重构的代码开始。首先要做的是在pom.xml中替换cucumber-picocontainer和PicoContainer 上的依赖关系:
如果你现在运行mvn clean test,你会看到如下的一些错误:
这是告诉我们的是,Guice试图实例化我们的一个胶水类的实例,但它不知道使用哪个构造函数(即使它们每个都只有一个构造函数)。
@Inject注释                                
为了告诉Guice使用哪个构造函数,我们将@Inject注解添加到我们想要注入对象的构造函数中:
我们还必须在AtmUserInterface,AccountSteps,ServerHooks,TellerSteps和WebDriverHooks中注释构造函数。当我们运行mvn clean test时,我们的两个场景都失败了,但是出现了另一个错误:
这里发生了什么?被报告的错误是一个MySQLIntegrityConstraintViolationException,它正在被抛出,因为我们试图用相同的帐号创建多个帐号。该独特的,我们把数据库的约束将不会允许这一点,这就是错误的原因。                
这个问题的根本原因是范围。我们已经在PicoContainer和Guice之间产生了巨大的差异,接下来我们再来看看。
@ScenarioScoped注释                       
Cucumber-Guice文档说:              
不建议不要让步骤定义类没有作用域,因为这意味着Cucumber将为使用该步骤定义的场景中的每个步骤实例化类的新实例。[57]              
我们有两个潜在的范围可供选择:@ScenarioScoped和@Singleton。一般来说,我们将使用@ScenarioScoped,因为这可以确保在每个场景开始运行之前重置状态。              
让我们注释我们的步骤定义和钩子,以表明我们要为每个场景创建新的步骤。需要注释的类是AccountSteps,CashSlotSteps,TellerSteps,BackgroundProcessHooks,ResetHooks,ServerHooks和WebDriverHooks:
当我们运行mvn clean test时,我们得到:
我们已经修复了shutdownServer中的失败,但是我们仍然在尝试创建一个重复的账户。这是因为,虽然我们已经用@ScenarioScoped注解了粘合代码,但是 Guice每次需要注入时仍然创建每个注入类的新实例。由于我们使用注入来在几个对象之间共享相同的实例,我们也需要注释这些类。需要注释的类是AtmUserInterface,CashSlot,MyWebDriver和TestAccount。                      
提出问题的唯一类是CashSlot,因为它是我们生产代码的一部分,为了保持我们的测试框架的快乐,我们可能不想添加注释。一个替代方案是创建一个TestCashSlot即延伸 CashSlot,这使吉斯注释仅限于我们的测试代码:
但是,现在我们需要告诉Guice创建一个TestCashSlot的实例,而不是CashSlot。我们可以通过改变的签名做到这一点@Inject -annotated,目前采取的构造函数CashSlot:
现在测试通过了!
@Singleton注解                        
在上一节中,我们将@ScenarioScoped注释不加区分地应用于由Guice管理的每个类。这意味着将为每个场景运行创建一个新实例。这对我们目前来说不是问题,因为我们只有一个场景,但是这种方法可能是浪费的,特别是如果对象构建起来昂贵的话。               
另一个支持的作用域@Singleton告诉Cucumber只为所有将要运行的场景构造一个单独的实例。只有在确定对象不存储可能会影响其他场景结果的状态时,才应使用此策略。在我们的例子中,我们可以将它应用到我们的Web驱动程序MyWebDriver:
测试继续通过,现在当我们添加更多的功能,他们将共享相同的浏览器实例。            
Guice有更多聪明的东西可以做,所以请在线查看文档。

春天在你的步骤            
Spring是一个非常流行和非常大的框架。黄瓜与Spring集成,用于处理步骤创建和DI,不需要类路径上的整个Spring框架。目前的黄瓜春季版本是针对Spring 4构建的,目前仍在积极开发之中,所以在网上查看版本说明,看看未来版本的黄瓜春季的变化。     
切换DI容器        
我们从代码开始,因为它是在结束PicoContainer的几乎看不见。和以前一样,我们将更改pom.xml以引入对我们选择的DI框架Spring的最小依赖:
我们还将添加一个配置文件cucumber.xml作为测试资源:
在这里,我们告诉Spring哪些基础包要使用context:component-scan元素来扫描我们要注入的类。在这个例子中,我们对钩子,nicebank和支持包感兴趣。
一些Spring注释                                
Spring使用注释@Autowired来标识应该注入的内容,而不是其他DI容器使用的@Inject注释。不幸的是,当前的cucumber-spring集成仅支持步定义和钩类的无参数构造函数。这意味着我们将不得不使用字段注入,而不是我们在PicoContainer和Guice中使用的构造器注入。DI容器将注入的对象直接注入注释的字段,而不是具有注释的构造函数来存储传递给它的注入对象:
现在我们只需要告诉Spring我们的哪些类是注入的候选者,用@Component标记它们并将它们与正确的作用域关联起来。将黄瓜弹簧集成自动关联所有挂钩和步定义了一个名为范围黄瓜胶,但我们需要的是我们已经创建到与该范围过于共享状态的任何对象相关联:
现在我们已经完成了,我们所有的支持类都被识别为bean,而Spring将很高兴地实例化并按需注入。               
运行mvn clean test来检查它是否正常工作。
一些春季配置魔术        
Spring配置文件非常强大。在本节中,我们将看到如何使用配置文件来指定一个类是一个bean,而不必修改Java代码。      
豆与无参数构造函数                              
Spring提供了另一种方法来指定哪些类是注入的候选对象,通过在配置文件中将它们配置为bean,在我们的例子cucumber.xml中。由于我们没有提到有关构造函数参数的任何信息,因此这些类需要具有缺省(或无参数)构造函数:
现在我们可以从AtmUserInterface和MyWebDriver中删除注释,并且完全删除TestCashSlot(因为我们只是为了避免编辑生产代码而创建它)。但是,我们需要做更多的工作来删除TestAccount,因为我们必须将帐号传递给它的构造函数。一个解决方案是创建AccountFactory并配置Spring使用:
运行mvn clean test,场景如前所述。
具有参数的Bean构造函数          
在上一节中,我们看到了如何在cucumber.xml文件中将类配置为一个bean 。对于具有默认的无参数构造函数的类来说,这很好。当我们需要用Account的参数调用构造函数时,我们必须创建一个AccountFactory。让我们看看是否有另一种方式来做到这一点。看看MyWebDriver:



正如你所看到的,它与Selenium的EventFiringWebDriver唯一的不同之处在于构造函数被连接到使用Firefox浏览器。作为最后一个例子,让我们使用Spring来让我们完全摆脱MyWebDriver。我们将首先告诉Spring如何为我们的场景创建一个EventFiringWebDriver来使用:
现在,我们需要替换所有引用MyWebDriver一起引用在我们的测试代码EventFiringWebDriver。下面是WebDriverHooks的一个例子:


mvn clean test的           
另一个运行将验证我们的场景仍然通过。

处理失败            
我们已经提交了一个工作方案,并向我们的利益相关者展示 他们对我们迄今所做的工作感到满意,但他们想讨论我们如何处理一些常见的错误情况。一旦我们进行了这些讨论,就可以开始扩展我们的Web应用程序了,我们将看到一些如何编写强制发生错误的场景的例子,以便我们能够推出我们的错误处理功能。         
我们的运行场景描述了一个情况,一个有账户资金的客户使用自动取款机成功地取出了一些资金。如果计算和分配资金的机制发生故障会发生什么?或者如果ATM没有足够的钱来满足客户的要求?我们确信你可以想出很多其他需要考虑的情况,但这足以给你一个想法。          
现在我们将实施您在与利益相关方合作时捕获的场景。因为春天是如此受欢迎的DI容器,我们会从我们写的代码开始春季在你的脚步。我们不会花太多时间来描述代码,因为在本书的前面我已经学到了大部分内容,但是我们将在使用Selenium时描述新的功能。你总是可以直接跳到重用浏览器,如果你想跳过开发阶段。
有故障的ATM                        
我们决定处理的第一种情况是ATM在用户试图提取资金之后但在分配资金之前发生故障的地方。与我们的利益相关者合作,我们捕捉这个情景如下:
在这种情况下有两个新的步骤:一个注入到现金槽中的错误,另一个检查是否向用户显示正确的错误消息。在看到这两者之前,我们会看看我们是否可以做任何事情来改善场景的阅读方式。
注入故障          
当我们的CashSlot所代表的ATM机制出现问题时,我们希望确保我们的软件正常工作。我们不想改变我们的生产代码,所以我们创建了一个TestCashSlot(扩展了 CashSlot)来允许我们模拟一个错误:
由于我们使用的是Spring,所以我们可以对配置文件cucumber.xml进行简单的修改,将我们的测试类注入到我们的应用程序中:
现在,当我们的场景运行时,Spring将创建一个TestCashSlot实例,并将其注入到任何需要TestCashSlot或CashSlot的地方。当我们尝试从有故障的ATM取款时,分配将会引发异常。目前我们正在使用Java的RuntimeException,但是如果这不仅仅是一个例子,我们希望用一些更有意义的东西替代它。                            
我们可以使用injectFault方法随时向我们的TestCashSlot注入一个错误。我们从新的一步把这个称为“但是现金槽已经发展了一个错误”:
请记住,在执行此步骤定义时,TestCashSlot已经由Spring创建并连接到我们的应用程序中。我们只是简单地改变一个标志,以表示对于所有未来的呼叫,我们希望它表现得好像是错误的。
检查文字                    
如果ATM发生故障,我们希望向用户显示一条有用的信息。我们将使用Selenium WebDriver来检查是否显示正确的消息。要做到这一点,我们添加以下步骤定义:
所有这一步定义所做的是将责任委托给柜员执行,以检查是否正在显示所需的“无序”消息。我们不在步骤定义本身做任何复杂的工作,因为我们已经解释过,我们想要保持这个胶水代码层尽可能薄。我们做了检查在AtmUserInterface的UI上显示的文本的实际工作:
当然,我们第一次运行它会失败,因为我们还没有改变我们的生产代码。为了处理我们尝试使用错误的CashSlot时抛出的异常,我们修改了WithdrawalServlet上的doPost。它现在从捕获的异常中提取消息并将其显示给用户:
在这一点上,我们可以运行mvn clean test,我们的新场景将会通过。
重写场景          
看看我们的新情况。它对你有好处吗?有没有在它是任何细节偶然到所描述的行为?记住,这个场景是关于处理技术故障的。
我们对账户开始时的金额,客户试图提取多少资金或发生错误后的余额不感兴趣。我们要确定的是显示正确的错误信息,没有钱分配,并且客户的余额不受影响。                  
在我们与利益相关者讨论这个问题之后,我们将这个场景改写为:
这种情况已经被剥夺了一些附带的细节,使我们能够使用自然语言把注意力集中在真正重要的事情上。
自动柜员机资金不足        
接下来我们要解决的一个情况是,ATM包含的现金少于用户试图提取的现金。与我们的利益相关者合作,我们捕捉这个情景如下:
这种情况介绍了一个ATM包含有限的金钱的想法。我们需要实施一种方式来指定多少钱加载到自动柜员机,并检查我们有足够的资金,然后我们试图分配给客户的钱:
我们还需要确保我们原始的,成功的撤销方案继续工作。我们可以通过增加一个额外的步骤来加载ATM,但这是一个偶然的细节。相反,我们在我们的TestCashSlot中添加一个构造函数,以确保有足够的现金可用于任何不特别感兴趣的机器中有多少钱的场景:
我们现在可以运行mvn clean test并查看所有三个场景的通过。                   
 我们只增加了两个额外的场景,但我们的功能已经运行了很长时间。如果我们不做任何事情,那么在团队停止运行场景之前不久。接下来,我们将看看最简单的方法来加快速度。

重新使用浏览器                                    
我们的每个场景都是通过我们的Web UI来运行我们的应用程序,因此需要使用浏览器。目前,我们为每个场景启动一个新的Firefox实例,这需要相当长的时间。是否真的有必要,或者我们的场景都可以使用相同的Firefox实例吗?          
每个场景都与所有其他场景隔离很重要,但是浏览器本身的场景很少。在大多数情况下,只要您清除任何cookie,重复使用相同的浏览器实例对于所有情况都是相当安全的。在这个例子中,它更简单 - 我们没有cookies。    
使用Spring共享浏览器                       
我们使用Spring配置文件cucumber.xml来定义我们的EventFiringWebDriver。这只是一个微小的修改,以保持整个运行的黄瓜浏览器实例活着。在下面的XML中,我们只是从bean的定义中删除了属性scope =“cucumber-glue”:
现在,当Spring创建Web驱动程序的一个实例时,它将与整个Cucumber运行的默认范围相关联。尝试运行mvn clean test,您会发现这使得该功能的总体运行速度更快,因为我们无需等待浏览器启动每个场景。             
 如果您还需要删除cookie,则需要编写一个挂钩,以编程方式执行此操作,接下来我们将会看到。                      
使用关闭钩子                        
有一种更通用的方式来做一些清理,当一个JVM关闭不依赖于Spring:使用Java的addShutdownHook。以下代码来自Cucumber项目中的Webbit-Websockets-Selenium示例:[60]
我们创建一个驱动程序实例并将其存储在一个静态变量(第2行)中。这将由SharedDriver的所有实例共享。我们还创建了一个静态 线程(第3行),并将其添加到JVM关闭时应该调用的JVM的钩子列表(第11行)。               
如果您尝试手动关闭驱动程序,SharedDriver将检查呼叫是否来自已注册的CLOSE_THREAD(第20行)。如果不是,SharedDriver将通过异常报告错误。这可以防止您在执行黄瓜功能时无意中关闭浏览器。              
此示例还显示了如何在每个场景运行之前使用挂钩来清除cookie(第29行)。管理方法是Selenium提供的EventFiringWebDriver API的一部分。              
在我们的ATM机上驱除一些故障处理功能的同时,我们已经学会了在黄瓜运行结束时调用清理代码的一般方法。我们还使用Selenium来确保每个场景清除之前场景可能保存的任何cookie。现在我们需要向我们的用户展示我们已经做了什么,看看他们是否喜欢它!

掌握Selenium的一个方便的方法是自动执行一些沉闷,重复的任务,例如填写时间表或下载银行对账单。              
我们提供了一个简单的旧Java项目,您可以随意添加任何您希望自动化的项目。它的核心就是这个简单的Java类:
打赏
赞(0) 打赏
未经允许不得转载:同乐学堂 » Cucumber-java版真正的入门到精通(Day6)

特别的技术,给特别的你!

联系QQ:1071235258QQ群:710045715

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏

error: Sorry,暂时内容不可复制!