快速单元或声明式单元测试方法



你好!我叫Yuri Skvortsov,我们的团队在Rosbank从事自动化测试。我们的任务之一是开发使功能测试自动化的工具。



在本文中,我想谈谈一种解决方案,该解决方案被认为是解决其他问题的小型辅助实用程序,但最终变成了一个独立的工具。我们正在谈论Fast-Unit框架,该框架允许您以声明性的方式编写单元测试,并将单元测试的开发转变为组件构造函数。该项目主要是为了测试我们的主要产品Tladianta而开发的-Tladianta是一个统一的BDD框架,用于测试4个平台:台式机,Web,移动和Rest。



首先,测试自动化框架并不是一项常见的任务。但是,在这种情况下,它不是测试项目的一部分,而是独立的产品,因此我们很快意识到需要单元。



在第一阶段,我们尝试使用assertJ和Mockito等现成的工具,但很快就遇到了我们项目的某些技术功能:



  • Tladianta已经使用JUnit4作为依赖项,这使得使用不同版本的JUnit变得困难,并且使其与Before一起使用变得更加困难。
  • Tladianta包含用于不同平台的组件,它具有许多实体,这些实体在功能上“极其接近”,但是具有不同的层次结构和行为。
  • «» ( ) ;
  • , , , , ;
  • - (, Appium , , , );
  • , : Mockito .




最初,当我们刚学会替换驱动程序,创建伪造的Selenium元素并编写测试工具的基本体系结构时,测试如下所示:



@Test
public void checkOpenHint() {
    ElementManager.getInstance().register(xpath,ElementManager.Condition.VISIBLE,
ElementManager.Condition.DISABLED);
    new HintStepDefs().open(("");
    assertTrue(TestResults.getInstance().isSuccessful("Open"));
    assertTrue(TestResults.getInstance().isSuccessful("Click"));
}

@Test
public void checkCloseHint() {
    ElementManager.getInstance().register(xpath);
    new HintStepDefs().close("");
    assertTrue(TestResults.getInstance().isSuccessful("Close"));
    assertTrue(TestResults.getInstance().isSuccessful("Click"));
}


甚至像这样:



@Test
public void fillFieldsTestOld() {
    ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX,"//check-box","",
ElementManager.Condition.NOT_SELECTED);
        ElementManager.getInstance().register(ElementManager.Type.INPUT,"//input","");
        ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP, 
"//radio-group","");
        DataTable dataTable = new Cucumber.DataTableBuilder()
                .withRow("", "true")
                .withRow("", "not selected element")
                .withRow(" ", "text")
                .build();
        new HtmlCommonSteps().fillFields(dataTable);
        assertEquals(TestResults.getInstance().getTestResult("set"), 
ElementProvider.getInstance().provide("//check-box").force().getAttribute("test-id"));
        assertEqualsTestResults.getInstance().getTestResult("sendKeys"), 
ElementProvider.getInstance().provide("//input").force().getAttribute("test-id"));
        assertEquals(TestResults.getInstance().getTestResult("selectByValue"), 
ElementProvider.getInstance().provide("//radio-group").force().getAttribute("test-id"));
    }


找到上面的代码中正在测试的内容以及理解检查并不难,但是有大量的代码。如果包含用于检查和描述错误的软件,则将变得非常难以阅读。我们只是试图检查是否在所需对象上调用了该方法,而检查的真正逻辑却是极其原始的。为了编写这样的测试,您需要了解ElementManager,ElementProvider,TestResults,TickingFuture(用于在给定时间内实现元素状态变化的包装器)。这些组件在不同的项目中是不同的,我们没有时间同步更改。



另一个挑战是制定一些标准。我们的团队具有自动化的优势,我们许多人在开发单元测试方面没有足够的经验,尽管乍一看很简单,但是阅读彼此的代码却很费力。我们试图足够快地清算技术债务,当出现数百种此类测试时,变得难以维护。此外,结果证明该代码配置繁重,丢失了真正的检查,并且皮带太粗,导致我们测试了我们自己的皮带,而不是测试框架的功能。



当我们尝试将开发从一个模块转移到另一个模块时,很明显,我们需要带出通用功能。那时,这个想法诞生了,不仅是创建具有最佳实践的库,而且是在此工具中创建单个单元开发过程。



改变哲学



如果从整体上看代码,您会发现许多代码块“毫无意义”地重复了。我们测试方法,但始终使用构造函数(以避免缓存某些错误的可能性)。第一次转换-我们将检查和测试实例的生成移到了注释中。



@IExpectTestResult(errDesc = "    set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    sendKeys", value = "sendKeys", 
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@Test
public void fillFieldsTestOld() {
    ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX, "//check-box", "",
ElementManager.Condition.NOT_SELECTED);
    ElementManager.getInstance().register(ElementManager.Type.INPUT, "//input", "");
    ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP, 
"//radio-group", "");
    DataTable dataTable = new Cucumber.DataTableBuilder()
            .withRow("", "true")
            .withRow("", "not selected element")
            .withRow(" ", "text")
            .build();
    runTest("fillFields", dataTable);
}


发生了什么变化?



  • 这些检查已委托给一个单独的组件。现在,您无需了解项目的存储方式,测试结果。
  • : errDesc , .
  • , , , – runTest, , .
  • .
  • - , .


我们喜欢这种形式的表示法,因此我们决定以相同的方式简化另一个复杂的组件-元素的生成。我们的大多数测试都是针对现成的步骤进行的,我们必须确保它们能够正确运行,但是,对于此类检查,有必要完全“启动”虚假应用程序并将其填充元素(回想一下我们在谈论Web,台式机和移动设备,差异很大)。



@IGenerateElement(type = ElementManager.Type.CHECK_BOX)
@IGenerateElement(type = ElementManager.Type.RADIO_GROUP)
@IGenerateElement(type = ElementManager.Type.INPUT)
@Test
@IExpectTestResult(errDesc = "    set", value = "set", 
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    sendKeys", value = "sendKeys", 
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = "    selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
public void fillFieldsTest() {
    DataTable dataTable = new Cucumber.DataTableBuilder()
            .withRow("", "true")
            .withRow("", "not selected element")
            .withRow(" ", "text")
            .build();
    runTest("fillFields", dataTable);
}


现在,测试代码已完全成为模板,参数清晰可见,所有逻辑均移至模板组件。默认属性使删除空行成为可能,并为重载提供了充足的机会。此代码几乎与BDD方法,前提条件,检查,操作一致。此外,所有绑定均已脱离测试的逻辑,您不再需要了解管理器,测试结果的存储,代码简单易读。由于Java中的注释几乎不可定制,因此我们为转换器引入了一种机制,该机制可以从字符串接收最终结果。此代码不仅检查调用方法的事实,还检查执行该方法的元素的ID。当时几乎存在的几乎所有测试(200多个单元)都被迅速转移到此逻辑,从而将它们带到一个模板中。测试已成为应有的条件-文档,不是代码,因此我们采用了声明式。正是这种方法构成了Fast-Unit的基础-声明性,自文档测试和测试功能的隔离,该测试完全致力于检查一种测试方法。



我们继续发展



现在,必须增加在项目框架内独立创建此类组件的能力,并增加控制其操作顺序的能力。为此,我们开发了阶段的概念:与Junit不同,所有这些阶段都独立存在于每个测试中,并在测试时执行。作为默认实现,我们制定了以下生命周期:



  • Package-generate-处理与package-info相关的注释。与它们关联的组件提供配置下载和常规线束准备。
  • 类生成-处理与测试类关联的注释。在此执行与框架相关的配置操作,以使其适应准备的绑定。
  • 生成-处理与测试方法本身相关的注释(入口点)。
  • 测试-准备实例并执行被测方法。
  • 断言-执行检查。


将要处理的注释描述如下:



@Target(ElementType.PACKAGE) //  
@IPhase(value = "package-generate", processingClass = IStabDriver.StabDriverProcessor.class,
priority = 1) //    (      )
public @interface IStabDriver {

    Class<? extends WebDriver> value(); //   ,     

    class StabDriverProcessor implements PhaseProcessor<IStabDriver> { // 
        @Override
        public void process(IStabDriver iStabDriver) {
            //  
        }
    }
}


快速单元功能是可以覆盖任何类的生命周期-ITestClass批注对此进行了描述,该批注旨在指示测试中的类和阶段。相列表仅以字符串数组形式指定,允许更改组成和相序。还可以通过注释找到处理阶段的方法,因此可以在类中创建必要的处理程序并对其进行标记(此外,可以在类中进行覆盖)。最大的好处是,这样的划分可以将测试分成几层:如果完成的测试中发生错误,则在包装生成或生成阶段发生错误,则测试线束会损坏。如果使用类生成-框架的配置机制存在问题。如果在测试框架内,则测试功能存在错误。从技术上讲,测试阶段可以在绑定和被测试的功能上引发错误,因此我们将可能的绑定错误包装在特殊类型-InnerException中。



每个阶段都是隔离的,即不依赖于其他阶段且与其他阶段不直接交互,因此在阶段之间传递的唯一东西是错误(如果先前阶段发生错误,则将跳过大多数阶段,但这不是必须的,例如,肯定阶段仍然可以工作)。



在这里,可能已经出现了问题,测试实例从何而来。如果构造函数为空,这是显而易见的:使用Reflection API,您只需创建要测试的类的实例。但是,如何在构造函数触发后在此构造中传递参数或配置实例?如果对象是由构建器构建的,或者通常是关于静态测试的,该怎么办?为此,已经开发了提供程序的机制,这隐藏了构造函数的复杂性。



默认参数化:



@IProvideInstance
CheckBox generateCheckBox() {
    return new CheckBox((MobileElement) ElementProvider.getInstance().provide("//check-box")
.get());
}


没有参数-没问题(我们正在测试CheckBox类并注册一个将为我们创建实例的方法)。由于此处默认的提供程序已被覆盖,因此无需在测试本身中添加任何内容,它们将自动使用此方法作为源。这个例子清楚地说明了快速单位逻辑-我们隐藏了复杂而不必要的东西。从测试的角度来看,与CheckBox类包装在一起的移动元素的来源和来源都无关紧要。对我们来说重要的是,存在一些满足指定要求的CheckBox对象。



自动参数注入:假设我们有一个像这样的构造函数:



public Mask(String dataFormat, String fieldFormat) {
    this.dataFormat = dataFormat;
    this.fieldFormat = fieldFormat;
}


然后使用参数注入对该类进行测试将如下所示:



Object[] dataMask={"_:2_:2_:4","_:2/_:2/_:4"};

@ITestInstance(argSource = "dataMask")
@Test
@IExpectTestResult(errDesc = "  ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
    runTest("convert","12102012");
}


命名提供商



最后,如果需要多个提供程序,则使用名称绑定,不仅隐藏了构造函数的复杂性,而且还显示了其真实含义。可以像这样解决相同的问题:



@IProvideInstance("")
Mask createDataMask(){
    return new Mask("_:2_:2_:4","_:2/_:2/_:4");
} 

@ITestInstance("")
@Test
@IExpectTestResult(errDesc = "  ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
    runTest("convert","12102012");
}


IProvideInstance和ITestInstance是关联的注释,它们使您可以告诉方法在哪里获取被测实例(对于静态,它仅返回null,因为该实例最终通过Reflection API使用)。提供程序方法提供了有关测试中实际发生情况的更多信息,用一些描述前提条件的文本替换了对构造函数的调用,因此,如果构造函数突然更改,我们将只需要更正提供程序,但是在实际功能更改之前,测试将保持不变。如果在审阅过程中看到多个提供程序,则应注意它们之间的差异,因此要注意所测试方法的行为特点。即使完全不了解框架,但仅了解快速单元操作的原理,开发人员将能够阅读测试代码并了解测试方法的作用。



结论与结果



事实证明,我们的方法具有许多优点:



  • 易于测试的可移植性。
  • 隐藏了绑定的复杂性,可以在不破坏测试的情况下重构它们。
  • 向后兼容性得到保证-方法名称的更改将记录为错误。
  • 测试已变成每种方法的相当详细的文档。
  • 检查质量已大大提高。
  • 单元测试的开发已成为流水线过程,开发和审查的速度已大大提高。
  • 已开发测试的稳定性-尽管框架和Fast-Unit本身正在积极开发,但测试不会降低


尽管表面上看起来很复杂,但是我们仍然能够快速实现此工具。现在,大多数单元都已写入其中,并且它们已经通过相当复杂和大量的迁移确认了它们的可靠性,它们能够识别出相当复杂的缺陷(例如,在等待元素和文本检查时)。我们能够迅速消除技术债务,并与部门建立有效的合作关系,从而使它们成为开发的组成部分。现在,我们正在考虑在团队之外的其他项目中更积极地实施此工具的选项。



当前的问题和计划:



  • , . , ( - ).
  • .
  • .
  • , -.
  • Fast-Unit junit4, junit5 testng



All Articles