从0到1搭建Web端的Ui自动化测试框架

一、为何要做Ui自动化

很多公司都把自动化的测试重心放在了接口自动化上,这无可厚非的,因为它效率快,覆盖率基本能做到100%以上,能把人力从重复的黑盒测试中解脱开来,让我们能把更多的人力花在用户交互体验,功能失败与异常,以及探索性上的测试,从而能更好的保证产品质量。

但是它的缺点也是显而易见的,因为它并未涉及 到 ui 层面的测试,在前后端分离,且ui 交互功能变得复杂的情况下,有些页面特别是有地图或动画的渲染,是存在着性能上的问题导致无法加载,另外也可能存在接口数据量大导致页面无法正常加载的,还有经常遇到的就是数据的不一致性等等。

这些问题直接暴露在客户面前,而且是无法通过接口自动化巡检来发现,这样我们就需要把Ui自动化测试也加入测试体系,在产品质量的保障上进行双重提供,并且也支持上线后对产品进行巡检和监控的需求。


二、实现Ui自动化的痛点与它的解决思路

痛点无非就是Ui的频繁修改导致控件的定位维护成本高,执行的稳定性差,对测试工程师的技术能力是要求会语言编程,投入和产出比例低。但这几个痛点有的是可以避免和解决。

Ui频繁修改导致维护成本高,是因为代码上没有做到良好的分层设计,包括数据与代码的分离,如果能做到很好的分层设计和数据和代码分离,那么即便是Ui频繁变动,那么改的也只是控件元素这层的代码,其他代码不受影响,维护成本相对应降低。

执行的稳定性差,这基本是体现在本地机器上跑的自动化,机器内存不足,或者网络因素都有可能影响,所以需要在linux服务器上部署同样一套UI自动化环境,在Linux上面跑相对于本地更稳定。

对测试工程师能力要求高,首先需要定期持续的对工程师进行相关的语言编程基础的进行培训,学习和考虑,让工程师能尽快上手基本的语言编程,其次在搭建好Ui自动化框架的过程中,尽可能的对常用的方法进行封装,继承,更重要的是使用分层设计思想让代码简洁易读,并让测试数据和代码分离,增加注释,减低普通测试工程师上手的难度。

投入与产出比例低,可以对核心业务的页面,核心数据的校验,或者是经常暴露问题的页面上,都有针对性的进行Ui自动化上的投入,这样相对于投入,它产出的效果就会极大。


三、框架搭建


image.png

四、环境准备与搭建



image.png

五、建立maven工程

上面准备的是本地的环境,需要配置好java环境和maven,我们使用的是java语言,所以可用IDEA开发代码平台,其他ecplise也可,配置好maven就行。

最后还需要下载浏览器的驱动工具配置在本地,是对应本地浏览器的版本号,我这边使用的是谷歌浏览器。

同样的操作在linux环境也需要配置和部署,包括java环境,maven工具,浏览器的驱动包。

使用maven工程的好处就是,它能打包下载好我们所需要的插件,包括我们本人的男主角Selenium,单元测试框架TestNg,日志系统Log4j,测试报告reportNG等等。

1.新建一个项目 Project name: 填写项目名称; Project location: 填写项目在硬盘上的路径;

2.将项目转成 maven 工程 项目上右键点击 Add Framework Support,然后勾选 Maven,点击 ok。

3.打开 pom.xml 进行配置


	<dependency>   //加入selenium做webUI测试,选用selenium
            <groupId>org.seleniumhq.selenium</groupId><artifactId>selenium-java</artifactId><version>2.53.1</version></dependency><dependency> //依赖Guice
            <groupId>com.google.inject</groupId><artifactId>guice</artifactId><version>3.0</version><scope>test</scope></dependency>
<dependency>//引入testng单元测试框架
            <groupId>org.testng</groupId><artifactId>testng</artifactId><version>7.0.0-beta7</version><scope>compile</scope>
</dependency>
<dependency>//引入日志系统
            <groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version>
</dependency>
<plugin>//添加插件,添加ReportNg的监听器,修改最后的TestNg的报告
                <groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.5</version><configuration><properties><property><name>usedefaultlisteners</name><value>false</value></property><property><name>listener</name><value>org.uncommons.reportng.HTMLReporter</value></property></properties><workingDirectory>target</workingDirectory><!-- <forkMode>always</forkMode> --></configuration>
</plugin>

六、分层设计

通常 ui 自动化的设计模式是:元素层+操作层+业务层,我们这边则采取的是 PageFactory 进行分层设计,基类层+页面层+业务操作层+测试用例层,再加上工具层+日志 系统+配置文件,扩展功能丰富,设计如下:


image.png

从表可知,需要经常维护的是页面类,比如页面元素变换等。至于业务测试数据文件,操作类和测试用例类则要看是否有变动才确定是否需要维护,下面是实现项目结构图:



image.png


七、工具类的封装和解析

正所谓磨刀不误砍柴工,在开始各目录进行编码之前,我们需要先开发好相关工具类,方便代码上的引用。

1.DriverUtil 类

DriverUtil 在 util 目录,是用于获取本地或服务器上的浏览器的驱动插件位置,并定 义驱动的属性。

		public class DriverUtil {
    public static WebDriver getDriver(){

        WebDriver driver=null;

        if (System.getProperty("os.name").contains("Windows")){
            System.setProperty("webdriver.chrome.driver", "c://chromedriver.exe");
            driver= new ChromeDriver();
            driver.manage().window().maximize();
        }else{
            System.setProperty("webdriver.chrome.driver", "/usr/bin/chromedriver");
            ChromeOptions chromeOptions=new ChromeOptions();
            chromeOptions.addArguments("headless");
            chromeOptions.addArguments("no-sandbox");//#解决DevToolsActivePort文件不存在的报错
            chromeOptions.addArguments("window-size=1920x3000"); //#指定浏览器分辨率
            chromeOptions.addArguments("disable-gpu"); //#谷歌文档提到需要加上这个属性来规避bug
            chromeOptions.addArguments("hide-scrollbars"); //#隐藏滚动条, 应对一些特殊页面
            chromeOptions.addArguments("blink-settings=imagesEnabled=false");// #不加载图片, 提升速度
            driver = new ChromeDriver(chromeOptions);
        }
        return driver;
    }
}

2.Assertion 类

Assertion 在 util 目录,用于断言预期的结果与测试的结果。

public class Assertion {
    static boolean flag=true;
    public static void verifyAssert(Object actual, Object expected)throws InterruptedException{
        try{
            Thread.sleep(200);
            Assert.assertEquals(actual,expected);
        }catch (Error e){
            flag=false;
        }
    }
    public static void verifyAssert(Object actual, Object expected, String message)throws InterruptedException{
        try{
            Thread.sleep(200);
            Assert.assertEquals(actual,expected,message);
            Log.info(message+"的断言成功了");
        }catch (Error e){
            flag=false;
            Log.error(message+"的断言失败了");
        }
    }




    public static void verifyAssertNotNull(Object actual)throws InterruptedException{
        try{
            Thread.sleep(200);
            Assert.assertNotNull(actual);
        }catch (Error e){
            flag=false;
        }
    }
    public static void verifyAssertNotNull(Object actual,String message)throws InterruptedException{
        try{
            Thread.sleep(200);
            Assert.assertNotNull(actual,message);
            Log.info(message+"不为空的断言成功了");
        }catch (Error e){
            flag=false;
            Log.error(message+"不为空的断言失败了");
        }
    }
    public static void verifyAssertNotEquals(Object obj1,Object obj2,String message)throws InterruptedException{
        try{
            Thread.sleep(200);
            Assert.assertNotEquals(obj1,obj2);
            Log.info(message+"不匹配的断言成功了");
        }catch (Error e){
            flag=false;
            Log.error(message+"不匹配的断言失败了");
        }
    }
}

3.PathUtils 类

PathUtils 在 util 目录,辨识项目是存在本地 window 还是服务器 linux 系统。

public class Pathutils {
    public static String getProjectRootPath(String windowsProjectPath, String linuxProjectPath) {
        if (System.getProperty("os.name").contains("Windows")) {
            return windowsProjectPath;
        } else {
            return linuxProjectPath;
        }
    }
}

4.ReadProperties 类


ReadProperties 在 util 目录类,用于读取配置文件信息.


public class ReadProperties {
  private String filePath;
  private Properties properties;
  /**
   构造方法 创建对象时自动返回pro对象  在new该对象的时候会自动加载readProperties()方法
    */public ReadProperties(String filePath){
    this.filePath = filePath;
    //在new该对象的时候会自动加载readProperties()方法this.properties = readProperties();
  }
  /**
   返回已经加载properties文件的pro对象
    */public Properties readProperties(){
    //创建对象
    Properties pro = new Properties();
    //读取properties文件到缓存try {
      BufferedInputStream in = new BufferedInputStream(new FileInputStream(filePath));
      //加载缓存到pro对象
      pro.load(in);
    } catch (Exception e) {
      e.printStackTrace();
    }
    //返回pro对象return pro;
  }
  /**
   使用全局的properties对象获取key对应的value值
    @return 
   * */public String getValue(String key){
    return properties.getProperty(key);
  }
}

6.RootPathUtil 类


RootPathUtil 在 util 目录类,用于封装window系统环境和linux系统环境的配置文件路径和日志路径。

   

public class RootPathUtil {
    public static String getDataPath(){
        String window = "D:\\evsmc\\rbtp-web\\configs\\testData.properties";
        String linux = "/var/lib/jenkins/workspace/rbtp-web-ui-test/configs/testData.properties";
        String rootPath=Pathutils.getProjectRootPath(window, linux);
        return  rootPath;
    }
    public static String getLogPath(){
        String window1 = "D:\\evsmc\\rbtp-web\\configs\\log4j.properties";
        String linux1 = "/var/lib/jenkins/workspace/rbtp-web-ui-test/configs/log4j.properties";
        String logPath=Pathutils.getProjectRootPath(window1, linux1);
        return  logPath;
    }
}


八、configs目录


Configs 目录目前配置了 log4j.properties 和 testData.properties。其中 log4j.properties 设置日志打印配置, testData.properties 设置测试数据信息配置。

	

## log4j.properties


log4j.rootLogger = info,stdout,FILE
org.apache.log4j.DailyRollingFileAppender


log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern =%d{yyyy-MM-dd HH:mm:ss.sss} - [ %p ] %m%n

log4j.appender.FILE = org.apache.log4j.DailyRollingFileAppender
log4j.appender.FILE.File = logs/log4j.log
log4j.appender.FILE.Append = true
log4j.appender.FILE.Threshold = info
log4j.appender.FILE.layout = org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern =%d{yyyy-MM-dd HH:mm:ss.sss} - [ %p ]  %m%n

log4j.appender.dailyRollingFile = org.apache.log4j.DailyRollingFileAppender
log4j.appender.dailyRollingFile.File = logs/log4j.log
log4j.appender.dailyRollingFile.Append = true
log4j.appender.dailyRollingFile.Threshold = info
log4j.appender.dailyRollingFile.layout = org.apache.log4j.PatternLayout
log4j.appender.dailyRollingFile.layout.ConversionPattern =%d{yyyy-MM-dd HH:mm:ss}  - [ %p ]  %m%n

测试数据文件配置


##testData.properties
buyer=15992667873>feihong#####
buyer2=14585852323>abc123
seller=14500007788>abc123
wronguser=15992667873>abc456
url=http://xx.xx.x.xxx/home/home
....
....
....
....

九、DriverBase 类


DriverBase 类在 base 目录,单例模式实现,用于传递 WebDriver,设置系统变量,测试的浏览器,并定 义常用的浏览器操作方法,比如浏览器打开,关闭,后退,刷新,前进。另外还封装了各 page 的方法调用,注意,如新增的 page 类不在这里进行封装,则在业 务类无法给 driverBase 进行调用 page 的方法。

	public class DriverBase {
    /***
     * 读取配置文件并获取测试数据
     * @return
     */
    String rootPath = RootPathUtil.getDataPath();
    ReadProperties properties = new ReadProperties(rootPath);
    String url = properties.getValue("url");
    /***
     * 用来传递WebDriver
     * @return
     */private static WebDriver driver;


    public static WebDriver driver() {
        return driver;
    }


    /***
     * 设置系统变量,并设置chromedriver的路径为系统属性值
     * @return
     */
    public DriverBase() {
        driver = DriverUtil.getDriver();
        driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);//隐式等待10秒
        driver.manage().deleteAllCookies();
        PageFactory.initElements(driver, this);
    }


    /**
     * 打开浏览器
     */
    public void open() throws InterruptedException {
        driver.get(url);
        Log.info("打开xxx首页");
        Thread.sleep(500);
    }


    /**
     * 关闭浏览器
     */
    public void close() throws InterruptedException {
        driver.quit();
        Log.info("退出浏览器");
    }


    /**
     * 后退浏览器
     */
    public void back() throws InterruptedException {
        driver.navigate().back();
        Thread.sleep(1000);
        Log.info("返回上一级页面");
    }


    /**
     * 浏览器刷新
     */
    public void refresh() throws InterruptedException {
        driver.navigate().refresh();
        Log.info("刷新页面");
        Thread.sleep(1000);
    }


    public void getScreen(String filepath) {
        WebDriver augmentedDriver = new Augmenter().augment(driver());
        TakesScreenshot ts = (TakesScreenshot) augmentedDriver;
        File screenShotFile = ts.getScreenshotAs(OutputType.FILE);
        try {
            FileUtils.copyFile(screenShotFile, new File(filepath));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 切换到最新的标签页面
     */
    public void switchNewPage() {
        String currentHandle = driver.getWindowHandle();
        for (String handles : driver.getWindowHandles()) {
            if (handles.equals(currentHandle))
                continue;
            driver.switchTo().window(handles);
        }
        Log.info("切换最新页面成功!");
    }


    public LoginPage loginPage() {
        LoginPage loginPage = new LoginPage();
        return loginPage;
    }


    public HomePage homePage() {
        HomePage homePage = new HomePage();
        return homePage;
    }

十、element 目录


所有 element 类都存放在 element 目录,以 LoginElement 类为例,主要是封装了登录页的元素定义,调用元素的操作方法的接口,给页面类继承使用。


	

public interface LoginElement {


    /**
     * 获取界面元素
     *
     */String tel_input ="//input[contains(@placeholder,'请输入手机号')]";
    String password_input ="//input[contains(@placeholder,'请输入密码')]";
    String login_button ="//*[@id=\"__layout\"]/div/div[2]/div/div/div[2]/form/div[6]/div/button";
    String accout_Button ="//span[contains(text(),'进入账号中心')]";
    String login_wrong_msg ="//div[contains(text(),'账号或密码错误')]";


    void enterBuyerTel();
    void enterBuyer2Tel();
    void enterSellerTel();
    void enterWrongTel();
    void enterBuyerPassword() ;
    void enterSellerPassword() ;
    void sellerLogin() ;
    void buyerLogin() ;
    void buyer2Login() ;
    void clickLoginButtonRight() ;
    void clickLoginButtonWrong() ;
}


十一、page目录


所有 page 类都存放在 page 目录,以 LoginPage 类为例,主要是继承了元素接口类的元素和操作方法并实现,以及对操作结果的断言判断。

	public class LoginPage implements LoginElement {
    /**
     * 读取配置文件获取测试数据
     *
     */
    String rootPath = RootPathUtil.getDataPath();
    ReadProperties properties = new ReadProperties(rootPath);
    String buyer = properties.getValue("buyer");
    String buyer2 = properties.getValue("buyer2");
    String seller = properties.getValue("seller");
    String wrong_user=properties.getValue("wronguser");
  
    /**
     * 手机号输入框
     */
    @FindBy(xpath=tel_input)
    @CacheLookup
    WebElement telphoneInput;
    /**
     * 密码输入框
     */
    @FindBy(xpath=password_input)
    @CacheLookup
    WebElement passwordInput;
    /**
     * 登录按钮
     */
    @FindBy(xpath=login_button)
    @CacheLookup
   WebElement loginButton;
    /**
     * 首页的进入账号按钮
     */
    @FindBy(xpath=accout_Button)
    @CacheLookup
    WebElement enter_accout_Button;
    /**
     * 登录错误提示
     */
    @FindBy(xpath=login_wrong_msg)
    @CacheLookup
   WebElement wrong_msg;
    public LoginPage(){
        PageFactory.initElements(DriverBase.driver(), this);
    }
    /**
     * 输入正确的买家的手机号
     *
     */
    @SneakyThrows
    public void enterBuyerTel() {
        telphoneInput.clear();
        Thread.sleep(500);
        telphoneInput.sendKeys(buyer.split(">")[0]);
        Log.info("输入正确买家的手机号");
    }
    /**
     * 输入正确的买家14585852323的手机号
     *
     */
    public void enterBuyer2Tel() {
        telphoneInput.clear();
        telphoneInput.sendKeys(buyer2.split(">")[0]);
        Log.info("输入正确买家14585852323的手机号");
    }
    /**
     * 输入正确的卖家的手机号
     *
     */
    public void enterSellerTel() {
        telphoneInput.clear();
        telphoneInput.sendKeys(seller.split(">")[0]);
        Log.info("输入正确的卖家手机号");
    }
    /**
     * 输入错误的手机号
     *
     */
    public void enterWrongTel() {
        telphoneInput.clear();
        telphoneInput.sendKeys(wrong_user.split(">")[0]);
        Log.info("输入错误的手机号");
    }
    /**
     * 输入买家的密码
     *
     */
    @SneakyThrows
    public void enterBuyerPassword() {
        passwordInput.clear();
        passwordInput.sendKeys(buyer.split(">")[1]);
        Log.info("输入买家的密码");
    }
    /**
     * 输入买家14585852323的密码
     *
     */
    @SneakyThrows
    public void enterBuyer2Password() {
        passwordInput.clear();
        passwordInput.sendKeys(buyer2.split(">")[1]);
        Log.info("输入买家的密码");
    }
    /**
     * 输入卖家的密码
     *
     */
    @SneakyThrows
    public void enterSellerPassword() {
        passwordInput.clear();
        passwordInput.sendKeys(seller.split(">")[1]);
        Log.info("输入买家的密码");
    }
    /**
     * 成功登录买家
     *
     */
    @SneakyThrows
    public void sellerLogin() {
        telphoneInput.clear();
        Thread.sleep(500);
        telphoneInput.sendKeys(seller.split(">")[0]);
        Log.info("输入正确的卖家手机号");
        passwordInput.clear();
        passwordInput.sendKeys(seller.split(">")[1]);
        Log.info("输入买家的密码");
        loginButton.click();
        Log.info("点击登录");
        String text=enter_accout_Button.getText();
        Assertion.verifyAssertNotNull(text,"【点击登录按钮】");
    }
    /**
     * 成功登录买家
     *
     */
    @SneakyThrows
    public void buyerLogin() {
        telphoneInput.clear();
        Thread.sleep(500);
        telphoneInput.sendKeys(buyer.split(">")[0]);
        Log.info("输入正确买家的手机号");
        passwordInput.clear();
        passwordInput.sendKeys(buyer.split(">")[1]);
        Log.info("输入买家的密码");
        loginButton.click();
        Log.info("点击登录");
        String text=enter_accout_Button.getText();
        Assertion.verifyAssertNotNull(text,"【点击登录按钮】");
    }
    /**
     * 成功登录买家
     *
     */
    @SneakyThrows
    public void buyer2Login() {
        telphoneInput.clear();
        telphoneInput.sendKeys(buyer2.split(">")[0]);
        Log.info("输入正确买家2的手机号");
        passwordInput.clear();
        passwordInput.sendKeys(buyer2.split(">")[1]);
        Log.info("输入买家2的密码");
        loginButton.click();
        Log.info("点击登录");
        String text=enter_accout_Button.getText();
        Assertion.verifyAssertNotNull(text,"【点击登录按钮】");
    }
    /**
     * 成功登录
     *
     */
    @SneakyThrows
    public void clickLoginButtonRight() {
        loginButton.click();
        String text=enter_accout_Button.getText();
        Assertion.verifyAssertNotNull(text,"【点击登录按钮】");
    }
    /**
     * 失败登录
     *
     */
    @SneakyThrows
    public void clickLoginButtonWrong() {
        loginButton.click();
        String msg=wrong_msg.getText();
        Assertion.verifyAssert(msg,"账号或密码错误。","错误登录");
    }
}

十二、operation 目录


所有业务操作类都存放在 operarion 目录,以 LoginOperarion 类为例,主要是封装了 登录页面的业务操作场景,包括成功的买家登录,卖家登录,失败的买家登录等

	

public class LoginOperation {
    final static Log log= Log.getlogger(LoginOperation.class);
    DriverBase driverBase = new DriverBase();
    public static LoginOperation getInstance() {
        LoginOperation d=new LoginOperation();
        return d;
    }
    public void buyerRightLogin() throws InterruptedException{
        log.startTestCase("成功登录买家15992667873账号");
        driverBase.open();
        driverBase.homePage().clickLoginLinkText();
        driverBase.loginPage().buyerLogin();
        driverBase.close();
        log.endTestCase("成功登录买家15992667873账号");
    }
    public void buyer2RightLogin() throws InterruptedException{
        log.startTestCase("成功登录买家14585852323账号");
        driverBase.open();
        driverBase.homePage().clickLoginLinkText();
        driverBase.loginPage().buyer2Login();
        driverBase.close();
        log.endTestCase("成功登录买家14585852323账号");
    }
    public void sellerRightLogin() throws InterruptedException{
        log.startTestCase("成功登录卖家14500007788账号");
        driverBase.open();
        driverBase.homePage().clickLoginLinkText();
        driverBase.loginPage().sellerLogin();
        driverBase.close();
        log.endTestCase("成功登录卖家14500007788账号");
    }
    public void wrong_Login() throws InterruptedException{
        log.startTestCase("失败登录");
        driverBase.open();
        driverBase.homePage().clickLoginLinkText();
        driverBase.loginPage().enterWrongTel();
        driverBase.loginPage().enterBuyerPassword();
        driverBase.loginPage().clickLoginButtonWrong();
        driverBase.close();
        log.endTestCase("失败登录");
    }
}


十三、testcase 目录


所有测试用例类都存放在 testcase 目录,以 LoginTest 类为例,主要是封装了登录的 测试用例,包括成功的买家登录,卖家登录,失败的买家登录等。LoginTest 类

	

@Epic("登录页面测试")
public class LoginTest {
    /**
     * 买家登录
     */@Test@Description("成功的买家登录测试")
    @TmsLink("TestLogin_1")
    public void BuyerRightLogin() throws InterruptedException {
        LoginOperation.getInstance().buyerRightLogin();
    }
    /**
     * 卖家登录
     */
    @Test
    @Description("卖家登录测试")
    @TmsLink("TestLogin_2")
    public void SellerRightLogin() throws InterruptedException {
        LoginOperation.getInstance().sellerRightLogin();
    }
    /**
     * 错误登录
     */
//    @Test
    public void WrongLogin() throws InterruptedException {
        LoginOperation.getInstance().wrong_Login();
    }
}

十四、testng.xml

testng.xml 文件主要用以管理测试用例。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="All Test Suite">


    <test verbose="2" preserve-order="true" name="rbtp-web-ui-test"><classes>


            <class name="testcase.LoginTest"></class><class name="testcase.HomePageTest"></class><class name="testcase.AddBookingTest"></class><class name="testcase.AccountCenterTest">
</class>
            <class name="testcase.BuyerCenterTest"></class><class name="testcase.BuyerPurchaseListTest"></class><class name="testcase.BuyerMyCollectTest"></class><class name="testcase.GoodsDetailTest"></class><class name="testcase.GoodsListTest"></class><class name="testcase.MessageCenterTest"></class><class name="testcase.OrderSuccessTest"></class><class name="testcase.PurchaseDetailTest"></class><class name="testcase.PurchaseListTest"></class><class name="testcase.SellerCenterTest"></class><class name="testcase.SellerOrderTest"></class><class name="testcase.SellerGoodsTest"></class><class name="testcase.SellerPurchaseCollectTest"></class><class name="testcase.ShopDetailTest"></class><class name="testcase.MyCartTest"></class></classes></test>
</suite>

十五、整体项目完成如图

image.png

十六、上传代码到gitlab


image.png

十七、jenkins拉取代码自动运行测试


image.png

十八、Allure测试报告展示

image.png


十九、邮件通知

image.png


二十、总结


各页面也是从元素层,到页面层,再到业务层,最后到用例层逐步实现。整体架构也实现了数据与代码分离,另外可维护变动的基本只限于元素层上,其他上变动不大,整体代码的阅读性比较好,很容易就能找到对应的页面对应的元素和操作方法。


由于篇幅关系,本文这里没有讲解如何上传到代码到gitlab,并通过jenkins来拉取代码打包部署运行测试,后续有空可以再开一篇文来讲解linux这块的环境的配置+gitlab代码仓库的管理+jenkins的部署运行+Allure测试报告展示。



文章为作者独立观点,不代表BOSS直聘立场。未经账号授权,禁止随意转载。