由于脚本没找到任何Bug,我们在某些特定阶段的任务变成让它查找潜在的系统问题。
随机化方法1 – 裸随机
从业务角度来说,自动化解决方案中任何步骤都是有效的。因此探索式测试使得我们可以自由地在任何时间点执行任何步骤。这些步骤的混搭也很简单。我们需要在执行过少数几次测试后,遵循已实现步骤打造“随机”测试用例。
输入:解决方案中所有业务方法的数量,要生成的测试脚本数量,生成每个测试脚本所需步骤数量。
输出:类似于下列脚本:
myRandomCase_1(){
do_that();
do_bla();
verify_this();
}
很明显,算某些测试用例可能(甚至已经)成功运行,大部分依然会失败,因为大量用例实际上是在试图完成无效操作。如果还没执行过do_this(),那么verify_this()无疑会失败。
随机化方法2 – 有先决条件的随机方法
这种方式的想法在于只有在工作流中已包含先觉步骤后,才向工作流中加入后续步骤,但这需要对代码库进行必要的扩充,确保测试案例生成器可以理解并保证准确的序列。为此可在方法之上添加特性或注解:
@Reguires(do_this)
verify_this()
{…}
这样我们得到了:
myRandomCase_2(){
do_bla();
do_this();
verify_this(); //can be added, because prerequisite step is already in test
}
这是一种更可预测的方法。但如果do_this()和verify_that()需要在同一个Page1上执行,而do_bla()已经到了Page2又该怎样?
此时我们面临一个新问题:verify_that()会失败,因为无法找到执行所需的控制/上下文。
人工随机化方法3 – 上下文感知
测试生成器必须了解执行位置上下文(例如Web开发中的“页面”)。当然,此时也可以通过特性/注解为生成器提供活跃上下文。
@ReguiresContext(pageThis)
verify_this()
{…}
@ReguiresContext(pageThis)
do_this()
{…}
@ReguiresContext(pageThis)
@MovesContextTo(pageThat)
do_bla()
{…}
本例中do_this()和verify_this()不会放在将上下文改为pageThat的方法,或上下文为pageThat的方法之后。
因此我们可以得到一个类似下面这样的测试脚本:
myRandomCase_3(){
do_this();
do_bla();
do_that();
}
或者也可以通过方法链实现。假设业务方法返回的对象为页面,测试案例生成器会持续追踪执行“步骤”前后浏览器中显示的页面,因此可以确定需要调用验证或“步骤”方法的正确页面。这种方法需要额外检查以验证流程是否正确,但这个操作可以无须注解实现。
筛选恰当的用例
至此介绍的方法已经可以生成相当大量的测试用例。
主要问题在于,验证过程本身,以及验证失败的测试场景是否是应用程序内的Bug,而非自动化测试脚本逻辑导致的,这些工作也需要耗费大量时间。
因此可以实现一种“预言”类,借此预测所获得的结果是否满意,或是否代表任何错误信息,并且必要时可进行后续分析。然而本例我们选择了一个略微不同的方法。
可以通过下列这一套规则代表应用程序的失败是Bug引起的:
1.500错误或类似页面
2.JavaScript错误
3.“未知错误”或因为误用造成的类似的错误信息
4.应用程序日志中有关异常和/或错误情况的信息
5.发现与任何其他产品有关的错误
本例中,可在每个步骤执行完毕后验证应用程序状态。因此自动生成的脚本看起来是这样的:
myRandomCase_3(){
do_this();
validate_standard_rules();
do_bla();
validate_standard_rules();
do_that();
validate_standard_rules();
}
其中validate_standard_rules()方法可以搜索上文提到的各种问题。
注意:通过与OOP结合,这种方法会显得更为强大,可以检测出实际的Bug。在Page Object超类实现常规检查需要查找“常规问题”,例如JavaScript错误、日志中的应用程序错误等。对于与特定页面有关的合理检查,可以绕过这种方法额外增加针对具体页面的检查。
实验
为了进行实验,我们决定使用公开的邮件系统。考虑到Gmail和Yahoo的流行度,这些系统中所有存在的Bug都已被发现的可能性相当高。因此我们选择了ProtonMail。
Taking Over Random
假设自动化解决方案已经位,我们“采用”了Shiny系统的自动化测试机制:首先建立一个通用的Java/Selenium测试项目,其中包含几个使用Page Object模式实现的冒烟测试。随后按照佳实践,所有业务方法可以返回一个新的Page Object(针对业务方法结束时依然显示在浏览器中的页面)或当前Page Object,除非页面被更改。
为进行自动化探索式测试,我们增加了包含在explr.core包中的类,其中感兴趣的当属TestCaseGenerator和TesCaseExecutor。
TestCaseGenerator
为了生成新的“随机”测试用例,可以通过TestCaseGenerator类调用两个generateTestCase方法之一。这两个方法都能以参数的方式接受代表所生成测试用例中“步骤验证对”数量的整数。第二个方法还可额外接受一个代表要使用的“验证策略”数量的参数(第一个方法使用默认策略,本例为USE_PAGE_SANITY_VERIFICATIONS)。
验证策略代表在向测试用例添加“检查”步骤时所用的方法。目前我们有两个选项:
1.USE_RANDOM_VERIFICATIONS:第一个,同时也是明显的策略。该策略的想法在于,使用来自页对象的当前验证方法。但不足之处在于严重依赖上下文。例如:我们随机选择了一个方法来验证特定主题的消息是否存在。首先,我们必须知道要查找哪个主题。为此我们引入了@Default注解和DefaultTestData类。DefaultTestData包含的常规测试数据可用于随机测试。@Default注解可用于将该数据绑定给特定的方法参数。随后我们需要确保包含该主题的消息先于验证操作已存在(可在执行该规范的过程中,或之前的任何测试过程中创建)。为此可通过@Depends注解告诉TestCaseGenerator检查特定方法的调用,如果当前步骤之前没找到则直接添加。此外我们还需要确保消息没有在验证之前删除。我们发现对于生成的测试用例,依赖性问题大幅降低了随机化程度,并且这种方法的稳定性也无法满足要求。
2.USE_PAGE_SANITY_VERIFICATIONS:该策略可检查显而易见的应用程序失败,如显示了错误的页,错误信息,JavaScript错误,应用程序日志中的错误等。在依赖性方面这个策略更灵活,可在需要时实现针对具体页的检查,例如已经足够灵活到可以找出实际的Bug。目前我们将其用作默认的验证策略。
TestCaseGenerator类可按照类名搜索Page对象:每个名称中包含“Page”字符串的类都会被看作是页对象。页对象的所有公开方法会被视作业务方法。名称包含“Verify”字符串的业务方法会被视作验证,所有其他方法会被视作测试步骤。@IgnoreInRandomTesting注解可用于从列表中排除某些工具方法或整个页对象。
随后可从两个列表中随机选择方法生成测试用例:一个列表包含测试步骤,一个列表包含验证步骤(如果所选验证策略需要验证步骤的话)。选择第一个方法后,将检查其返回值是否为另一个页对象。如果返回值是另一个页对象,那么将从其方法中选择下一个步骤(参见上文备注)。为避免在两个页之间循环往复,有一成的概率会跳转至一个完全随机的页面。如果方法使用@Depends注解标注了任何依赖项,则会按需解决这些问题并添加。
为避免出现从当前所显示页之外其他对象调用测试方法的情况,生成的测试用例会传递一个额外的验证,借此添加缺少的导航调用。
TesCaseExecutor
生成之后,测试用例基本上是一种“类-方法对”列表,可通过特定方式执行或保存。尽管可在运行时执行,但从调试和后续分析的角度来看,保存为文件是一种更好的做法。
生成的测试用例可通过多种方式执行,可以TesCaseExecutor作为其接口,以SaveToFileExecutor作为的实现,借此可简单地创建一个代表所生成测试用例的.java文件。令人惊异的是,这种相当简单的解决方案完全满足了我们的需求:实现速度快,可对测试结果进行深入分析,并能了解具体的生成方式。的不足在于,必须手工编译并运行生成的测试用例,不过对于实验来说,这也算不得什么大问题。
SaveToFileExecutor生成的测试用例代码可通过模板转换为可编译的文件。这样生成的测试范例如下:
@Test(dataProvider = "WebDriverProvider")
public void test(WebDriver driver){
login(driver);
//****<Generated>****
ContactsPage contactspage = new ContactsPage(driver, true);
InboxMailPage inboxmailpage = contactspage.inbox();
inboxmailpage.sanityCheck();
ComposeMailPage composemailpage = inboxmailpage.compose();
composemailpage.sanityCheck();
composemailpage.setTo("
me@myself.com");
composemailpage.send();
inboxmailpage.sanityCheck();
List list = inboxmailpage.findBySubject("Seen that?");
inboxmailpage.sanityCheck();
inboxmailpage.inbox();
inboxmailpage.sanityCheck();
DraftsMailPage draftsmailpage = inboxmailpage.drafts();
draftsmailpage.sanityCheck();
inboxmailpage.inbox();
inboxmailpage.sanityCheck();
inboxmailpage.sendNewMessageToMe();
inboxmailpage.setMessagesStarred(true, "autotest", "Seen that?");
inboxmailpage.sanityCheck();
TrashMailPage trashmailpage = inboxmailpage.trash();
trashmailpage.sanityCheck();
//****</Generated>****
}
SaveToFileExecutor生成的代码位于<Generated>备注之间,其余代码由模板添加。
从所执行的操作方面来看,我们生成的用例多样化程度一般,但只要添加包含更多测试步骤的更多页对象即可轻松解决。
在进行过上千个“随机”测试后,我们发现Protonmail没什么大问题(例如错误页),但浏览器汇报了一些JavaScript错误,对于依赖JavaScript进行邮件编解码工作的系统,这些问题非常重要。很明显,整个实验中我们并不能访问服务器日志,但实验的角度来说,已经足够展示出这样的方法对被测试系统质量的促进能起到多大的作用。
当然,随机测试无法取代主观或传统测试技术,但可在回归测试过程中让我们对应用程序质量更为自信。