可能是UI测试的最佳架构



可能是在某处有理想的文章,该文章立即和完整地揭示了测试体系结构的主题,易于编写,阅读和维护,并为初学者提供了示例性的实现和应用程序领域。我只想在收到第一个任务“编写自动测试”之后,以我梦dream以求的格式提出对“理想文章”的看法。为此,我将讨论众所周知的和不太知名的Web自动测试方法,为什么,如何以及何时使用它们,以及有关存储和创建数据的成功解决方案。



哈Ha!我叫Diana,我是用户界面测试小组的负责人,我从事自动化Web和桌面测试已有五年了。代码示例将在Java和Web中使用,但是在实践中,已经对其进行了测试,这些方法适用于带有桌面的python。



一开始是...



刚开始时有一个单词,有很多单词,并且无论您的体系结构和DRY原理如何,它们都用代码均匀地填充了所有页面(不要重复自己-无需重复上面已经编写了三段的代码)。





实际上,堆积在屏幕上的“ footcloth”(又称“ sheet”)或非结构化代码的体系结构并不是很糟糕,并且很适用于以下情况:



  • 对于非常小的项目,快速单击三行(可以,两百三十三行);
  • 迷你演示中的代码示例;
  • 对于自动测试中“ Hello Word”样式的第一个代码。


要获得床单的结构需要做什么?只需将所有必要的代码写入一个文件,即一个普通画布。



import com.codeborne.selenide.Condition;
import com.codeborne.selenide.WebDriverRunner;
import org.testng.annotations.Test;

import static com.codeborne.selenide.Selenide.*;

public class RandomSheetTests {
    @Test
    void addUser() {
        open("https://ui-app-for-autotest.herokuapp.com/");
        $("#loginEmail").sendKeys("test@protei.ru");
        $("#loginPassword").sendKeys("test");
        $("#authButton").click();
        $("#menuMain").shouldBe(Condition.appear);

        $("#menuUsersOpener").hover();
        $("#menuUserAdd").click();

        $("#dataEmail").sendKeys("mail@mail.ru");
        $("#dataPassword").sendKeys("testPassword");
        $("#dataName").sendKeys("testUser");
        $("#dataGender").selectOptionContainingText("");
        $("#dataSelect12").click();
        $("#dataSelect21").click();
        $("#dataSelect22").click();
        $("#dataSend").click();

        $(".uk-modal-body").shouldHave(Condition.text(" ."));

        WebDriverRunner.closeWebDriver();
    }
}


如果您刚刚开始熟悉自动测试,那么“工作表”已经足够完成一个简单的测试任务,尤其是如果您对测试设计有很好的了解并具有很好的覆盖范围时。但这对于大型项目来说太容易了,因此,如果您有雄心壮志,但又没有时间理想地执行每个测试用例,那么至少您的gita应该有一个更复杂的体系结构示例。



页面对象



听说过PageObject过时的谣言?你只是不知道怎么做!



此模式中的主要工作单元是一个“页面”,即一整套元素以及与之相关的操作,例如MenuPage-一个用菜单描述所有操作的类,即单击选项卡,展开下拉菜单等。







为对象创建的模式窗口(简称“模式”)组成PageObject有点困难。类字段的集合很清楚:所有输入字段,复选框,下拉列表;对于方法,有两种选择:您可以使两种通用方法“填充所有模态字段”,“用随机值填充所有模态字段”,“检查所有modalk字段”,以及使单独的方法“填充名称”,“检查名称”, “填写说明”等等。在特定情况下使用什么取决于优先级-“整个模态的一种方法”方法提高了编写测试的速度,但是与“每个字段的一种方法”方法相比,它在测试的可读性上损失了很多。



让我们组成一个常见的Page Object,为两种类型的测试创建用户:

public class UsersPage {

    @FindBy(how = How.ID, using = "dataEmail")
    private SelenideElement email;
    @FindBy(how = How.ID, using = "dataPassword")
    private SelenideElement password;
    @FindBy(how = How.ID, using = "dataName")
    private SelenideElement name;
    @FindBy(how = How.ID, using = "dataGender")
    private SelenideElement gender;
    @FindBy(how = How.ID, using = "dataSelect11")
    private SelenideElement var11;
    @FindBy(how = How.ID, using = "dataSelect12")
    private SelenideElement var12;
    @FindBy(how = How.ID, using = "dataSelect21")
    private SelenideElement var21;
    @FindBy(how = How.ID, using = "dataSelect22")
    private SelenideElement var22;
    @FindBy(how = How.ID, using = "dataSelect23")
    private SelenideElement var23;
    @FindBy(how = How.ID, using = "dataSend")
    private SelenideElement save;

    @Step("Complex add user")
    public UsersPage complexAddUser(String userMail, String userPassword, String userName, String userGender, 
                                    boolean v11, boolean v12, boolean v21, boolean v22, boolean v23) {
        email.sendKeys(userMail);
        password.sendKeys(userPassword);
        name.sendKeys(userName);
        gender.selectOption(userGender);
        set(var11, v11);
        set(var12, v12);
        set(var21, v21);
        set(var22, v22);
        set(var23, v23);
        save.click();
        return this;
    }

    @Step("Fill user Email")
    public UsersPage sendKeysEmail(String text) {...}

    @Step("Fill user Password")
    public UsersPage sendKeysPassword(String text) {...}

    @Step("Fill user Name")
    public UsersPage sendKeysName(String text) {...}

    @Step("Select user Gender")
    public UsersPage selectGender(String text) {...}

    @Step("Select user variant 1.1")
    public UsersPage selectVar11(boolean flag) {...}

    @Step("Select user variant 1.2")
    public UsersPage selectVar12(boolean flag) {...}

    @Step("Select user variant 2.1")
    public UsersPage selectVar21(boolean flag) {...}

    @Step("Select user variant 2.2")
    public UsersPage selectVar22(boolean flag) {...}

    @Step("Select user variant 2.3")
    public UsersPage selectVar23(boolean flag) {...}

    @Step("Click save")
    public UsersPage clickSave() {...}

    private void set(SelenideElement checkbox, boolean flag) {
        if (flag) {
            if (!checkbox.isSelected()) checkbox.click();
        } else {
            if (checkbox.isSelected()) checkbox.click();
        }
    }
}


:



    @Test
    void addUser() {
        baseRouter.authPage()
                .complexLogin("test@protei.ru", "test")
                .complexOpenAddUser()
                .complexAddUser("mail@test.ru", "pswrd", "TESTNAME", "", true, false, true, true, true)
                .checkAndCloseSuccessfulAlert();
    }


:



    @Test
    void addUserWithoutComplex() {
        //Arrange
        baseRouter.authPage()
                .complexLogin("test@protei.ru", "test");
        //Act
        baseRouter.mainPage()
                .hoverUsersOpener()
                .clickAddUserMenu();
        baseRouter.usersPage()
                .sendKeysEmail("mail@test.ru")
                .sendKeysPassword("pswrd")
                .sendKeysName("TESTNAME")
                .selectGender("")
                .selectVar11(true)
                .selectVar12(false)
                .selectVar21(true)
                .selectVar22(true)
                .selectVar23(true)
                .clickSave();
        //Assert
        baseRouter.usersPage()
                .checkTextSavePopup(" .")
                .closeSavePopup();
    }


. : , , , , — . , , , .



最重要的是,所有带有页面的操作都封装在页面内(隐藏了实现,只有逻辑操作可用),因此,测试中已经使用了业务功能。这样一来,您就可以为每个平台(网络,台式机,手机)编写自己的页面,而无需更改测试。



唯一可惜的是,完全相同的接口在不同平台上很少见。



为了减少接口之间的差异,有一种使单个步骤复杂化的诱惑,它们被带到单独的中间类中,并且测试变得越来越不易读,最多分为两个步骤:“登录,做得好”,测试结束。除了Web之外,我们的项目中没有其他接口,而且我们要阅读案例要比编写案例更多,因此,出于可读性考虑,历史PageObjects获得了新的外观。



PageObject是每个人都知道的经典。您可以找到许多关于这种方法的文章,其中包含几乎所有编程语言的示例。PageObject的使用通常用于判断候选人是否对测试用户界面有所了解。使用这种方法执行测试任务是大多数雇主所期望的,并且即使在仅进行Web测试的情况下,很多任务仍存在于生产项目中。



还有什么呢?



奇怪的是,没有一个PageObject!



  • 通常会找到ScreenPlay模式,例如,您可以在此处阅读有关模式它没有在我国扎根,因为使用bdd方法而不让无法阅读代码的人参与是对自动化者的毫无意义的暴力。
  • js- , PageObject, - , , .
  • - , , ModelBaseTesting, . , .


我将更详细地告诉您有关Page元素的信息,它使您可以减少相同类型代码的数量,同时提高可读性并提供对测试的快速理解,即使对于那些不熟悉项目的人也是如此。并在其上(当然还有其二十一点和首选项!)构建了流行的非js框架htmlElements,Atlas和Epam的JDI。



什么是页面元素?



为了构建页面元素模式,让我们从最低级别的元素开始。正如Wiktionary所说,“小部件”是具有标准外观并执行标准动作的图形用户界面的软件原语。例如,最简单的小部件“按钮”-您可以单击它,可以检查文本和颜色。在“输入字段”中,您可以输入文本,检查输入的文本,单击,检查焦点显示,检查输入的字符数,输入文本并按“ Enter”,检查占位符,检查“强制性”字段和错误文本的高亮显示,仅此而已,在特定情况下可能还需要其他什么。此外,此字段的所有操作在任何页面上都是标准的。







有一些较复杂的小部件,其动作不太明显,例如,目录树。编写它们时,您需要基于用户在程序的这一部分上所做的工作,例如:



  • 单击目录中具有指定文本的元素,
  • 检查具有给定文本的元素的存在,
  • 检查具有给定文本的元素的缩进。


窗口小部件可以有两种类型:在构造函数中具有定位符,并且在没有更改功能的情况下将定位符缝在窗口小部件中。目录通常是页面上的一个目录,其搜索方法可以留在目录的动作中,将定位器分开是没有意义的,因为定位器可能会从外部意外损坏,但是单独存储并没有任何好处。反过来,文本字段是通用的,相反,您只需通过构造函数中的定位器来使用它,因为一次可以有很多输入字段。如果出现至少一种仅用于一个特殊输入字段的方法,例如,另外单击下拉提示,则该方法不再只是一个输入字段,是时候为其创建自己的窗口小部件了。



为了减少总体混乱,将小部件(如页面元素)组合到相同的页面中,显然,组成页面元素的名称。



public class UsersPage {

    public Table usersTable = new Table();

    public InputLine email = new InputLine(By.id("dataEmail"));
    public InputLine password = new InputLine(By.id("dataPassword"));
    public InputLine name = new InputLine(By.id("dataName"));
    public DropdownList gender = new DropdownList(By.id("dataGender"));
    public Checkbox var11 = new Checkbox(By.id("dataSelect11"));
    public Checkbox var12 = new Checkbox(By.id("dataSelect12"));
    public Checkbox var21 = new Checkbox(By.id("dataSelect21"));
    public Checkbox var22 = new Checkbox(By.id("dataSelect22"));
    public Checkbox var23 = new Checkbox(By.id("dataSelect23"));
    public Button save = new Button(By.id("dataSend"));

    public ErrorPopup errorPopup = new ErrorPopup();
    public ModalPopup savePopup = new ModalPopup();
}


要使用上面在测试中创建的所有内容,您需要依次引用页面,小部件,操作,因此我们得到以下构造:



    @Test
    public void authAsAdmin() {
        baseRouter
                .authPage().email.fill("test@protei.ru")
                .authPage().password.fill("test")
                .authPage().enter.click()
                .mainPage().logoutButton.shouldExist();
    }


如果框架中需要此步骤,则可以添加经典的步骤层(例如,Java的RobotFramework远程库的实现需要一个步骤类作为输入),或者要添加漂亮的报表注释。我们使它成为基于注释的生成器,如果您有兴趣,请在注释中编写,我们将告诉您。



授权步骤类的示例
public class AuthSteps{

    private BaseRouter baseRouter = new BaseRouter();

    @Step("Sigh in as {mail}")
    public BaseSteps login(String mail, String password) {
        baseRouter
                .authPage().email.fill(mail)
                .authPage().password.fill(password)
                .authPage().enter.click()
                .mainPage().logoutButton.shouldExist();
        return this;
    }
    @Step("Fill E-mail")
    public AuthSteps fillEmail(String email) {
        baseRouter.authPage().email.fill(email);
        return this;
    }
    @Step("Fill password")
    public AuthSteps fillPassword(String password) {
        baseRouter.authPage().password.fill(password);
        return this;
    }
    @Step("Click enter")
    public AuthSteps clickEnter() {
        baseRouter.authPage().enter.click();
        return this;
    }
    @Step("Enter should exist")
    public AuthSteps shouldExistEnter() {
        baseRouter.authPage().enter.shouldExist();
        return this;
    }
    @Step("Logout")
    public AuthSteps logout() {
        baseRouter.mainPage().logoutButton.click()
                .authPage().enter.shouldExist();
        return this;
    }
}
public class BaseRouter {
//    ,      ,     
    public AuthPage authPage() {return page(AuthPage.class);}
    public MainPage mainPage() {return page(MainPage.class);}
    public UsersPage usersPage() {return page(UsersPage.class);}
    public VariantsPage variantsPage() {return page(VariantsPage.class);}
}




这些步骤与页面内的步骤非常相似,实际上没有什么不同。但是将它们分成单独的类将为代码生成打开范围,而不会丢失与相应页面的硬链接。同时,如果不在页面中编写步骤,则封装的含义消失,并且,如果不向pageElement中添加步骤类,则与页面的交互仍然与业务逻辑分开。



, , . . , , , « , ». — , page object , !





在不涉及方便地使用测试数据的方法的情况下谈论项目的体系结构是错误的。



最简单的方法是“按原样”或通过变量直接在测试中传递数据。这对于工作表架构很好,但是大型项目会变得凌乱。



另一种方法是将数据存储为对象,这对我们来说是最好的方法,因为它可以在一个地方收集与一个实体相关的所有数据,从而消除了将所有内容混合在一起并在错误的地方使用东西的诱惑。此外,此方法还具有许多其他改进,这些改进对单个项目很有用。



对于每个实体,都会创建一个描述它的模型,在最简单的情况下,该模型包含字段的名称和类型,例如,这是用户模型:



public class User {
    private Integer id;
    private String mail;
    private String name;
    private String password;
    private Gender gender;

    private boolean check11;
    private boolean check12;
    private boolean check21;
    private boolean check22;
    private boolean check23;

    public enum Gender {
        MALE,
        FEMALE;

        public String getVisibleText() {
            switch (this) {
                case MALE:
                    return "";
                case FEMALE:
                    return "";
            }
            return "";
        }
    }
}


生活技巧#1:如果您具有类似客户端服务器交互的架构(客户端或服务器之间使用json或xml对象,而不是无法读取的代码段),则可以将json谷歌搜索到<您的语言>对象,可能您所需的生成器已经存在...



人生#2:如果您的服务器开发人员使用相同的面向对象编程语言编写代码,则可以使用他们的模型。



人生#3:如果您是个笨手笨脚,并且公司允许您使用第三方库,并且周围没有紧张的同事,那么对于使用附加库而不是纯净美丽的Java的异端主义者来说,他们会感到非常痛苦,请使用Lombok!是的,通常是IDE可以生成getter,setter,toString和builders。但是,当比较我们的Lombok模型和没有Lombok的开发模型时,可以看到数百行不包含每个类业务逻辑的“空”代码的收益。使用Lombok时,您不必打败那些将字段和getter与setter混合使用的人,该类更易于阅读,您可以一次了解对象,而无需滚动三个屏幕。



因此,我们具有需要在其上拉伸测试数据的对象的线框。数据可以存储为最终的静态变量,例如,这对于创建其他用户的主要系统管理员很有用。最好使用final,这样就不会在测试中更改数据,因为下一个测试(而不是管理员)可以拥有一个“无能为力”的用户,更不用说并行执行测试了。



public class Users {
    public static final User admin = User.builder().mail("test@protei.ru").password("test").build();
}


要获取不影响其他测试的数据,可以使用“原型”模式并在每个测试中克隆您的实例。我们决定使其变得更容易:编写一种使类的字段随机化的方法,如下所示:



    public static User getUserRandomData() {
        User user = User.builder()
                .mail(getRandomEmail())
                .password(getShortLatinStr())
                .name(getShortLatinStr())
                .gender(getRandomFromEnum(User.Gender.class))
                .check11(getRandomBool())
                .check21(getRandomBool())
                .check22(getRandomBool())
                .check23(getRandomBool())
                .build();
//business-logic: 11 xor 12 must be selected
        if (!user.isCheck11()) user.setCheck12(true); 
        if (user.isCheck11()) user.setCheck12(false);
        return user;
    }


同时,可将产生直接随机性的方法更好地放在单独的类中,因为它们也将在其他模型中使用:







在获取随机用户的方法中,使用了“ builder”模式,这是必需的,以便不为每个所需的集合创建新的构造函数类型领域。当然,您可以简单地调用所需的构造函数。



这种存储数据的方法使用“值对象”模式,根据项目的需要,您可以在此基础上添加您的任何愿望。您可以将保存对象添加到数据库,从而在测试前准备系统。您不能随机分配用户,但可以从属性文件(以及一个很酷的库)中加载用户)。您可以在任何地方使用同一用户,但可以为每种类型的对象创建所谓的数据注册表,其中将端到端计数器的值添加到对象的名称或其他唯一字段中,并且测试将始终具有自己的唯一testUser_135。



您可以编写自己的对象存储(Google对象池和flyweight),并可以在测试开始时从中请求必要的实体。仓库给出其准备工作的对象之一并将其标记为已占用。在测试结束时,对象将返回到存储中,必要时将其清理干净,标记为空闲,然后交给下一个测试。如果创建对象的操作非常消耗资源,则可以执行此操作,并且使用这种方法,存储可以独立于测试工作,并且可以为以下情况准备数据。



资料建立



对于用户编辑案例,您肯定会需要一个要编辑的已创建用户,并且通常,编辑测试并不关心该用户来自何处。有几种创建方法:



  • 测试前用手按一下按钮,
  • 保留先前测试的数据,
  • 在从备份进行测试之前进行部署,
  • 通过直接在测试中单击按钮来创建
  • 使用API​​。


所有这些方法都有缺点:如果您需要在测试之前手动在系统中输入某些内容,那么这将是一个糟糕的测试,因此被称为自动测试,因为它们应尽可能独立于人的手来工作。



使用先前测试的结果违反了原子性原则,并且不允许您单独运行测试,您将必须运行整个批处理,并且ui测试的速度并不那么快。编写测试被认为是一种很好的形式,这样每个人都可以出色地隔离运行,而无需额外的舞蹈。另外,创建对象时放弃先前测试的错误根本不能保证编辑中的错误,在这种设计中,编辑测试将在下一次失败,并且无法找出编辑是否有效。



将备份(数据库的已保存映像)与测试所需的数据一起使用已经不是一种好方法,尤其是如果备份是自动部署的,或者测试本身将数据放入数据库中时。但是,为什么在测试中使用此特定对象并不明显,所以数据交叉问题也可以从大量测试开始。有时由于数据库体系结构的更新,备份将无法正常工作,例如,如果您需要在旧版本上运行测试,并且备份已经包含新字段。您可以通过为应用程序的每个版本组织备份存储来解决此问题。有时由于数据库体系结构更新而使备份不再有效-定期显示新字段,因此需要定期更新备份。突然间可能是这样一个单一的备份用户永远不会崩溃,并且如果只是创建了该用户或为该用户随机分配了名称,那么您会发现一个错误。这称为“农药效应”,该测试停止捕获错误,因为该应用程序“习惯”了相同的数据并且不会掉落,并且侧面没有偏差。



如果通过在同一界面上单击来在测试中创建用户,则农药会减少,并且用户外观的非显而易见性也会消失。缺点类似于使用先前测试的结果:速度一般,并且即使创建中存在错误,即使是最小的错误(尤其是测试错误,例如,保存按钮的定位器都会改变),那么我们将不知道编辑是否有效。



最后,创建用户的另一种方法是通过测试中的http-API,即不要单击按钮,而是立即发送创建所需用户的请求。因此,农药被尽可能地减少了,用户来自何处是显而易见的,并且创建速度比单击按钮时要高得多。这种方法的缺点是它不适用于客户端与服务器之间的通信协议中没有json或xml的项目(例如,如果开发人员使用gwt编写并且不想为测试人员编写其他api)。使用API​​时,可能会丢失管理面板执行的逻辑并创建无效的实体。 API可能会更改,从而导致测试失败,但是通常这是已知的,并且没有人需要更改才能更改,很可能这是仍然需要检查的新逻辑。API级别上也可能会出现错误,但是除了现成的备份以外,没有其他方法可以安全地做到这一点,因此最好结合使用多种方法来创建数据。



添加一滴API



在准备数据的方法中,最适合我们的方法是,http-API可以满足当前单独测试的需求,并为测试中不会更改的其他测试数据部署备份,这些对象在测试中不会更改,例如对象的图标,以便这些对象的测试在加载图标时不会崩溃。



为了通过Java中的API创建对象,事实证明使用restAssured库是最方便的,尽管它并不是真的打算这样做。我想分享一些发现的芯片,您知道更多-写!



第一个难题是系统中的授权。需要为每个项目分别选择其方法,但是有一件共同的事情-需要在请求规范中放置授权,例如:



public class ApiSettings {
    private static String loginEndpoint="/login";

    public static RequestSpecification testApi() {
        RequestSpecBuilder tmp = new RequestSpecBuilder()
                .setBaseUri(testConfig.getSiteUrl())
                .setContentType(ContentType.JSON)
                .setAccept(ContentType.JSON)
                .addFilter(new BeautifulRest())
                .log(LogDetail.ALL);
        Map<String, String> cookies = RestAssured.given().spec(tmp.build())
                .body(admin)
                .post(loginEndpoint).then().statusCode(200).extract().cookies();
        return tmp.addCookies(cookies).build();
    }
}


您可以添加为特定用户保存Cookie的功能,然后发送到服务器的请求数量将减少。此方法的第二个可能的扩展是保存接收到的当前测试的Cookies,并跳过授权步骤,将其扔给浏览器驱动程序。奖金是几秒钟,但是如果将它们乘以测试次数,则可以加快速度!



有一个发bun的步态和漂亮的报告,请注意以下几点 .addFilter(new BeautifulRest())



BeautifulRest班级


public class BeautifulRest extends AllureRestAssured {
        public BeautifulRest() {}

        public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext filterContext) {
            AllureLifecycle lifecycle = Allure.getLifecycle();
            lifecycle.startStep(UUID.randomUUID().toString(), (new StepResult()).setStatus(Status.PASSED).setName(String.format("%s: %s", requestSpec.getMethod(), requestSpec.getURI())));
            Response response;
            try {
                response = super.filter(requestSpec, responseSpec, filterContext);
            } finally {
                lifecycle.stopStep();
            }
            return response;
        }
}




对象模型与restAssured完全匹配,因为库本身可以处理json / xml中的模型的序列化和反序列化(从json / xml格式转换为给定类的对象)。



    @Step("create user")
    public static User createUser(User user) {
        String usersEndpoint = "/user";
        return RestAssured.given().spec(ApiSettings.testApi())
                .when()
                .body(user)
                .post(usersEndpoint)
                .then().log().all()
                .statusCode(200)
                .body("state",containsString("OK"))
                .extract().as(User.class);
    }


如果您连续考虑创建对象的几个步骤,则可以注意到代码的身份。要减少相同的代码,可以编写用于创建对象的常规方法。



    public static Object create(String endpoint, Object model) {
        return RestAssured.given().spec(ApiSettings.testApi())
                .when()
                .body(model)
                .post(endpoint)
                .then().log().all()
                .statusCode(200)
                .body("state",containsString("OK"))
                .extract().as(model.getClass());
    }

    @Step("create user")
    public static User createUser(User user) {
                  create(User.endpoint, user);
    }


再一次关于常规操作



作为检查对象编辑的一部分,我们通常不关心对象如何通过api或从备份中出现在系统中,或者是由ui测试创建的。重要的操作是找到一个对象,单击对象上的``编辑''图标,清除字段并用新值填充它们,单击``保存''并检查所有新值是否已正确保存。与测试没有直接关系的所有不必要的信息应在单独的方法中删除,例如在步骤类中。



    @Test
    void checkUserVars() {        
//Arrange
        User userForTest = getUserRandomData();
       
 //         , 
 //      -  , 
 //   ,   
        usersSteps.createUser(userForTest);
        authSteps.login(userForTest);
       
 //Act
        mainMenuSteps
                .clickVariantsMenu();
       
 //Assert
        variantsSteps
                .checkAllVariantsArePresent(userForTest.getVars())
                .checkVariantsCount(userForTest.getVarsCount());
        
//Cleanup
        usersSteps.deleteUser(userForTest);
    }


重要的是不要被遗忘,因为仅由“复杂”动作组成的测试变得不那么可读,并且在不深入研究代码的情况下更难以重现。



    @Test
    void authAsAdmin() {
        authSteps.login(Users.admin);
//  ,    .     . 
//   ,   ? 


如果套件中实际上出现了相同的测试,只是在数据准备上有所不同(例如,您需要检查所有三种类型的“不同”用户都可以执行相同的操作,或者有不同类型的控件对象,则需要检查每种控件)创建相同的依存对象,或者需要按十种对象状态检查筛选),仍然不能将重复的部分移到单独的方法中。如果可读性对您很重要,那根本就不!



相反,您需要阅读有关数据驱动测试的信息,对于Java + TestNG,它将是这样的:



    @Test(dataProvider = "usersWithDifferentVars")
    void checkUserDifferentVars(User userForTest) {
        //Arrange
        usersSteps.createUser(userForTest);
        authSteps.login(userForTest);
        //Act
        mainMenuSteps
                .clickVariantsMenu();
        //Assert
        variantsSteps
                .checkAllVariantsArePresent(userForTest.getVars())
                .checkVariantsCount(userForTest.getVarsCount());
    }

 //         . 
 // ,   -.
    @DataSupplier(name = "usersWithDifferentVars")
    public Stream<User> usersWithDifferentVars(){
        return Stream.of(
            getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(false),
            getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(false),
            getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(false),
            getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(true),
            getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(false),
            getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(true),
            getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(true),
            getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(true)
        );
    }


它使用Data Supplier,它是TestNG Data Provider的附加组件,它使您可以使用类型化的集合而不是Object [] [],但是本质是相同的。因此,我们得到一个测试,该测试与接收输入数据一样执行多次。



结论



因此,要创建一个大型但方便的用户界面自动测试项目,您需要:



  • 描述在应用程序中找到的所有小部件,
  • 将小部件收集到页面中,
  • 为各种实体创建模型,
  • 添加方法以基于模型生成各种实体,
  • 考虑一种用于创建其他实体的合适方法
  • 可选:手动生成或收集步骤文件,
  • 编写测试,以便在特定测试的主要操作部分中,没有复杂的操作,只有带有小部件的明显操作。


完成后,您已经创建了一个基于PageElement的项目,该项目具有用于存储,生成和准备数据的简单方法。现在,您拥有一个易于维护,可管理且足够灵活的体系结构。有经验的测试人员和6月的初学者都可以轻松浏览项目,因为以用户操作形式进行的自动测试最容易阅读和理解。



来自文章的代码示例(以完成的项目的形式)被添加到git中



All Articles