Selenium+java - 手把手一起搭建一个最简单自动化测试框架

写在前面

我们刚开始做自动化测试,可能写的代码都是基于原生写的代码,看起来特别不美观,而且感觉特别生硬。

来看下面一段代码,如下图所示:

从上面图片代码来看,具体特征如下:

  • driver对象在测试类中显示
  • 定位元素的value值在测试类中显示
  • 定位元素的方式(By对象)在测试类中显示
  • 代码一报错,还要去测试类里面找是哪段代码报错,要是代码行数几百行时呢,就不好定位问题了
  • 这样的测试脚本组装批量执行,批量报错后,怒号排查,定位问题,很吃力

看看自己有几条命中呢,其他现象就不一一列举了。

1、为什么要写自动化测试框架

从个人方面来说:

  • 逼格高,让别人感觉你好厉害,技术强
  • 面试是加分项,会写框架,可以作为谈资硬性指标
  • 一个组内,要是妹纸多,你可以秀技能,吸引妹纸也说不定呢
  • 对自己而言,更是一种挑战和提升

从实际方面来说:

  • 提高脚本编写效率,脚本的可读性,可降低代码维护成本
  • 方便定位问题,失败定位问题会比较方便
  • 可以提升测试效率,编写脚本成本,拿来就用,直接点方法就行
  • 减少了手工测试成本,节省人力成本

2、框架设计思路与实现

框架的几大要素:driver,元素管理,脚本,数据,元素对象,LOG,报告,运行机制,失败用例

框架的分层思想:脚本,数据,元素对象分离,使用Page Object和Page Factory思想,这里我将框架分为四层:基础层、元素对象层、操作层、业务层。

这里我们以163邮箱登录为例,进行讲解。

具体代码目录结构如下:

下面我们将进入大家都比较关注的话题了,这里我只分享思路哈,跟上步伐,别掉队哦

2.1、log4j日志配置

关于log4j,这部分不是讲解重点,因为只需配置一次可以一直使用,下面的配置是拿来就能用的,有兴趣的同学可以自行百度去学习,本文只是为了使用log4j的日志效果。

添加配置文件log4j.properties,具体如下:

###根logger设置###
log4j.rootLogger = INFO,console,file

### 输出信息到控制台###
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.Threshold = info 
log4j.appender.console.layout.ConversionPattern = [%p] %d{yyyy-MM-dd HH:mm:ss} method: %l----%m%n

###输出INFO 级别以上的日志文件设置###
log4j.appender.file = org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File = D:/log/web.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} method: %l - [ %p ]----%m%nH

新建测试类,名为TestLog,具体示例代码如下:

package com.frame.demo.test;

import org.apache.log4j.Logger;
import org.testng.annotations.Test;

public class TestLog {
    /**
     * log4j日志
     */
    private static Logger logger = Logger.getLogger(TestLog.class);
    @Test
    public void testLog() {
        logger.info("this is info log!!");
        logger.error("this is error log!!");
    }
}

运行效果如下图:

2.2、基础层的实现

用于封装浏览器对象,这里只写了firefox 和 chrome两种浏览器,有兴趣的同学可自行扩展。

具体代码示例如下:

package com.frame.demo.base;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

/**
 * @author rongrong
 */
public class GetDriverUtil {

    /**
     * @param browersName
     * @return
     */
    public static WebDriver getDriver(String browersName) {
        if (browersName.equalsIgnoreCase("firefox")) {
            return new FirefoxDriver();
        } else {
            System.setProperty("webdriver.chrome.driver", "chromedriver.exe");
            return new ChromeDriver();
        }
    }

}

2.3、元素对象管理与实现

为什么要做元素对象管理,因为在一个页面中有几十个元素对象,而一个网站有多个页面,一旦页面元素发生变化,维护起来不是很方便,因此,我们可以把需要录入的定位元素集中地放在一个地方去管理维护,而不是分散在测试用例脚本(代码)中。

这样的好处就是,可以提升些许编写脚本速度,降低后期维护成本,这时如果UI变了,我们只需要修改对应的页面中的元素定位即可。

具体数据源格式如下:

2.31、解析Excel文件

我们可以使用xml、yaml、excel、csv、mysql等等去存储管理元素对象数据,鉴于我们测试同学习惯用Excel去编写维护用例,本案例就以Excel为数据源,来进行元素的管理和维护,其他文件去维护管理的,有兴趣的同学可以去尝试。

首先来解析Excel文件,在pom.xml中添加依赖,如下:

        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.16</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.16</version>
        </dependency>

解析excel文件,具体示例代码如下:

package com.frame.demo.utils;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ReadExcelUtil {


    public static Map<String, String> getElementData() {
        Workbook wb;
        Sheet sheet;
        Row row;
        List<Map<String, String>> list = null;
        String cellData;
        Map<String, String> map = new LinkedHashMap<String, String>();
        wb = readExcel("locator/data.xlsx");
        if (wb != null) {
            sheet = wb.getSheetAt(0);
            int rownum = sheet.getPhysicalNumberOfRows();
            for (int i = 1; i < rownum; i++) {
                row = sheet.getRow(i);
                if (row != null) {
                    String name = (String) getCellFormatValue(row.getCell(0));
                    cellData = getCellFormatValue(row.getCell(1)) + "," + getCellFormatValue(row.getCell(2));
                    map.put(name, cellData);
                } else {
                    break;
                }
            }
        }
        return map;
    }


    public static Workbook readExcel(String filePath) {
        Workbook wb = null;
        if (filePath == null) {
            return null;
        }
        String extString = filePath.substring(filePath.lastIndexOf("."));
        InputStream is = null;
        try {
            is = new FileInputStream(filePath);
            if (".xls".equals(extString)) {
                return wb = new HSSFWorkbook(is);
            } else if (".xlsx".equals(extString)) {
                return wb = new XSSFWorkbook(is);
            } else {
                return wb = null;
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return wb;
    }

    public static Object getCellFormatValue(Cell cell) {
        Object cellValue = null;
        if (cell != null) {
            switch (cell.getCellType()) {
                case Cell.CELL_TYPE_NUMERIC: {
                    cellValue = String.valueOf(cell.getNumericCellValue());
                    break;
                }
                case Cell.CELL_TYPE_FORMULA: {
                    if (DateUtil.isCellDateFormatted(cell)) {
                        cellValue = cell.getDateCellValue();
                    } else {
                        cellValue = String.valueOf(cell.getNumericCellValue());
                    }
                    break;
                }
                case Cell.CELL_TYPE_STRING: {
                    cellValue = cell.getRichStringCellValue().getString();
                    break;
                }
                default:
                    cellValue = "";
            }
        } else {
            cellValue = "";
        }
        return cellValue;
    }


}

可以在本地创建一个data.xlsx文件,保存在locator目录中,放在项目根目录下,通过调用getElementData方法,将解析后元素对象放到map中实现映射

读取配置文件config.properties,具体代码如下:

package com.frame.demo.utils;

import java.util.Locale;
import java.util.ResourceBundle;


public class BaseInfo {

    public static String getBrowserType() {
        return getInfo("browserType");
    }

    public static String getUrl() {
        return getInfo("url");
    }

    public static String getInfo(String key) {
        ResourceBundle bundle = ResourceBundle.getBundle("config", Locale.CHINA);
        String value = bundle.getString(key);
        return value;
    }
}

2.32、By对象的封装

在写脚本时,元素对象一般是这样写的 WebElement element = driver.findElement(By.name(value);,接下来我们要把excel解析出来的Map中的"value"转换成 By 对象,添加如下代码:

 private By getBy(String method, String methodValue) {
        if (method.equals("id")) {
            return By.id(methodValue);
        } else if (method.equals("name")) {
            return By.name(methodValue);
        } else if (method.equals("xpath")) {
            return By.xpath(methodValue);
        } else if (method.equals("className")) {
            return By.className(methodValue);
        } else if (method.equals("linkText")) {
            return By.linkText(methodValue);
        } else if (method.equals("css")) {
            return By.cssSelector(methodValue);
        } else {
            return By.partialLinkText(methodValue);
        }
    }

这样通过map中的key和 value 的值就对产生一个 By 对象,接着可以把这个对象传给 driver.findElement 方法,然后我们在进行WebElement 对象的封装

    private By getBy(String method, String methodValue) {
        if (method.equalsIgnoreCase("id")) {
            return By.id(methodValue);
        } else if (method.equalsIgnoreCase("name")) {
            return By.name(methodValue);
        } else if (method.equalsIgnoreCase("tagName")) {
            return By.tagName(methodValue);
        } else if (method.equalsIgnoreCase("className")) {
            return By.className(methodValue);
        } else if (method.equalsIgnoreCase("linkText")) {
            return By.linkText(methodValue);
        } else if (method.equalsIgnoreCase("xpath")) {
            return By.xpath(methodValue);
        } else if (method.equalsIgnoreCase("cssSelector")) {
            return By.cssSelector(methodValue);
        } else {
            return By.partialLinkText(methodValue);
        }
    }


    public WebElement findElement(String name) {
        String data = elementData.get(name).toString();
        String method = data.split(",")[0];
        String methodValue = data.split(",")[1];return driver.findElement(this.getBy(method, methodValue));
    }

这回再来看一下,是不是感觉有那么点意思了,代码整洁了许多,到这里一个简单的元素管理部分的实现就算基本完成了,这只是提供一个思路,来个总结性的代码。

package com.frame.demo.page;

import com.frame.demo.base.GetDriverUtil;
import com.frame.demo.utils.BaseInfo;
import com.frame.demo.utils.ReadExcelUtil;
import org.apache.log4j.Logger;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;

import java.util.Map;

public class BasePage {
    private static Logger logger = Logger.getLogger(BasePage.class);
    static WebDriver driver;
    Map<String, String> elementData;

    public BasePage() {
        String browserType = BaseInfo.getBrowserType();
        driver = GetDriverUtil.getDriver(browserType);
        driver.manage().window().maximize();
        PageFactory.initElements(driver, this);
        elementData = ReadExcelUtil.getElementData();
    }

    private By getBy(String method, String methodValue) {
        if (method.equalsIgnoreCase("id")) {
            return By.id(methodValue);
        } else if (method.equalsIgnoreCase("name")) {
            return By.name(methodValue);
        } else if (method.equalsIgnoreCase("tagName")) {
            return By.tagName(methodValue);
        } else if (method.equalsIgnoreCase("className")) {
            return By.className(methodValue);
        } else if (method.equalsIgnoreCase("linkText")) {
            return By.linkText(methodValue);
        } else if (method.equalsIgnoreCase("xpath")) {
            return By.xpath(methodValue);
        } else if (method.equalsIgnoreCase("cssSelector")) {
            return By.cssSelector(methodValue);
        } else {
            return By.partialLinkText(methodValue);
        }
    }
    
    public WebElement findElement(String name) {
        String data = elementData.get(name).toString();
        String method = data.split(",")[0];
        String methodValue = data.split(",")[1];
        logger.info("获取元素控件 " + name);
        return driver.findElement(this.getBy(method, methodValue));
    }


    public void switchToFrame(int frame) {
        driver.switchTo().frame(frame);
    }

    public void open() {
        String url = BaseInfo.getUrl();
        logger.info("打开163邮箱首页");
        driver.get(url);
    }
    
    public void quit() {
        logger.info("关闭浏览器成功!");
        driver.quit();
    }
}

 整合了一部分driver对象的操作,仅供参考,有兴趣的同学可以接着拓展

2.4、元素对象层的再封装

接着我们再来创建一个类,名为Action继承BasePage,用于存放页面元素定位和常用控件操作的封装,具体示例代码如下:

package com.frame.demo.action;

import com.frame.demo.page.BasePage;

public class Action extends BasePage {

    public void sendKeys(String name, String str) {
        findElement(name).clear();
        findElement(name).sendKeys(str);
    }

    public void click(String name) {
        findElement(name).click();
    }

    public String getText(String name) {
        return findElement(name).getText();
    }
}

2.5、操作层的实现

接着我们再来创建一个类,名为LoginPage继承Action,用来记录登录的一系列操作,具体示例代码如下:

package com.frame.demo.object;

import com.frame.demo.action.Action;
import org.testng.Assert;
public class LoginPage extends Action {
    public void login(String userName, String pwd, String expected) throws Exception {
        open();
        click("密码登录");
        switchToFrame(0);
        sendKeys("输入用户名", userName);
        sendKeys("输入密码", pwd);
        click("点击登录");
        Thread.sleep(1000);
        String msg = getText("错误提示信息");
        Assert.assertEquals(msg, expected);
        quit();
    }
}

2.6、业务层的实现

最后我们再来创建一个类,名为TestFrame继承LoginPage,用来验证登录功能,具体示例代码如下:

package com.frame.demo.test;

import com.frame.demo.object.LoginPage;
import org.testng.annotations.Test;

public class TestFrame extends LoginPage {

    @Test
    public void textLogin() throws Exception {
       login("your userName", "your passWord", "帐号格式错误");
    }
}

到此基本可以说是,一个最简单的自动化测试框架算完成了一大半啦,是不是感觉很开心。哈哈

2.7、测试报告的美化

笔者之前用过testNG自带的测试报告、优化过reportNG的测试报告、Zreport(大飞总原创),Allure2,这些是我之前都用过的,这里我想用extentreport做演示讲解,因为界面的dashboard很好看,毕竟始于颜值嘛,哈哈,好了,废话不多说。

具体步骤如下:

在maven项目pom.xml文件添加如下内容:

   <dependency>
            <groupId>com.relevantcodes</groupId>
            <artifactId>extentreports</artifactId>
            <version>2.41.1</version>
        </dependency>

        <dependency>
            <groupId>com.vimalselvam</groupId>
            <artifactId>testng-extentsreport</artifactId>
            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.aventstack</groupId>
            <artifactId>extentreports</artifactId>
            <version>3.0.6</version>
        </dependency>

然后新建一个类,名为ExtentTestngReporterListener,用来编写监听类,监听测试执行过程中哪些测试成功,哪些失败,写入报告中,具体示例代码如下:

package com.frame.demo.report;

import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.ResourceCDN;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.model.TestAttribute;
import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
import com.aventstack.extentreports.reporter.configuration.ChartLocation;
import org.testng.*;
import org.testng.xml.XmlSuite;

import java.io.File;
import java.util.*;

/**
 * @author rongrong
 */
public class ExtentTestngReporterListener implements IReporter {
    //生成的路径以及文件名
    private static final String OUTPUT_FOLDER = "test-output/";
    private static final String FILE_NAME = "index.html";
    private ExtentReports extent;

    @Override
    public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
        init();
        boolean createSuiteNode = false;
        if (suites.size() > 1) {
            createSuiteNode = true;
        }
        for (ISuite suite : suites) {
            Map<String, ISuiteResult> result = suite.getResults();
            //如果suite里面没有任何用例,直接跳过,不在报告里生成
            if (result.size() == 0) {
                continue;
            }
            //统计suite下的成功、失败、跳过的总用例数
            int suiteFailSize = 0;
            int suitePassSize = 0;
            int suiteSkipSize = 0;
            ExtentTest suiteTest = null;
            //存在多个suite的情况下,在报告中将同一个一个suite的测试结果归为一类,创建一级节点。
            if (createSuiteNode) {
                suiteTest = extent.createTest(suite.getName()).assignCategory(suite.getName());

            }
            boolean createSuiteResultNode = false;
            if (result.size() > 1) {
                createSuiteResultNode = true;
            }
            for (ISuiteResult r : result.values()) {
                ExtentTest resultNode;
                ITestContext context = r.getTestContext();
                if (createSuiteResultNode) {
                    //没有创建suite的情况下,将在SuiteResult的创建为一级节点,否则创建为suite的一个子节点。
                    if (null == suiteTest) {
                        resultNode = extent.createTest(r.getTestContext().getName());
                    } else {
                        resultNode = suiteTest.createNode(r.getTestContext().getName());
                    }
                } else {
                    resultNode = suiteTest;
                }
                if (resultNode != null) {
                    resultNode.getModel().setName(suite.getName() + " : " + r.getTestContext().getName());
                    if (resultNode.getModel().hasCategory()) {
                        resultNode.assignCategory(r.getTestContext().getName());
                    } else {
                        resultNode.assignCategory(suite.getName(), r.getTestContext().getName());
                    }
                    resultNode.getModel().setStartTime(r.getTestContext().getStartDate());
                    resultNode.getModel().setEndTime(r.getTestContext().getEndDate());
                    //统计SuiteResult下的数据
                    int passSize = r.getTestContext().getPassedTests().size();
                    int failSize = r.getTestContext().getFailedTests().size();
                    int skipSize = r.getTestContext().getSkippedTests().size();
                    suitePassSize += passSize;
                    suiteFailSize += failSize;
                    suiteSkipSize += skipSize;
                    if (failSize > 0) {
                        resultNode.getModel().setStatus(Status.FAIL);
                    }
                    resultNode.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;", passSize, failSize, skipSize));
                }
                buildTestNodes(resultNode, context.getFailedTests(), Status.FAIL);
                buildTestNodes(resultNode, context.getSkippedTests(), Status.SKIP);
                buildTestNodes(resultNode, context.getPassedTests(), Status.PASS);
            }
            if (suiteTest != null) {
                suiteTest.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;", suitePassSize, suiteFailSize, suiteSkipSize));
                if (suiteFailSize > 0) {
                    suiteTest.getModel().setStatus(Status.FAIL);
                }
            }

        }

        extent.flush();
    }

    private void init() {
        //文件夹不存在的话进行创建
        File reportDir = new File(OUTPUT_FOLDER);
        if (!reportDir.exists() && !reportDir.isDirectory()) {
            reportDir.mkdir();
        }
        ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME);
        htmlReporter.config().setDocumentTitle("自动化测试报告");
        htmlReporter.config().setReportName("自动化测试报告");
        htmlReporter.config().setChartVisibilityOnOpen(true);
        htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP);
        htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
        htmlReporter.config().setCSS(".node.level-1  ul{ display:none;} .node.level-1.active ul{display:block;}");
        extent = new ExtentReports();
        extent.attachReporter(htmlReporter);
        extent.setReportUsesManualConfiguration(true);
    }

    private void buildTestNodes(ExtentTest extenttest, IResultMap tests, Status status) {
        //存在父节点时,获取父节点的标签
        String[] categories = new String[0];
        if (extenttest != null) {
            List<TestAttribute> categoryList = extenttest.getModel().getCategoryContext().getAll();
            categories = new String[categoryList.size()];
            for (int index = 0; index < categoryList.size(); index++) {
                categories[index] = categoryList.get(index).getName();
            }
        }

        ExtentTest test;

        if (tests.size() > 0) {
            //调整用例排序,按时间排序
            Set<ITestResult> treeSet = new TreeSet<ITestResult>(new Comparator<ITestResult>() {
                @Override
                public int compare(ITestResult o1, ITestResult o2) {
                    return o1.getStartMillis() < o2.getStartMillis() ? -1 : 1;
                }
            });
            treeSet.addAll(tests.getAllResults());
            for (ITestResult result : treeSet) {
                Object[] parameters = result.getParameters();
                String name = "";
                //如果有参数只取第一个参数作test-name
                for (int i = 0; i < parameters.length; i++) {
                    name = parameters[0].toString();
                }

                if (name.length() > 0) {
                    if (name.length() > 100) {
                        name = name.substring(0, 100) + "...";
                    }
                } else {
                    name = result.getMethod().getMethodName();
                }
                if (extenttest == null) {
                    test = extent.createTest(name);
                } else {
                    //作为子节点进行创建时,设置同父节点的标签一致,便于报告检索。
                    test = extenttest.createNode(name).assignCategory(categories);
                }
                for (String group : result.getMethod().getGroups()) {
                    test.assignCategory(group);
                }

                List<String> outputList = Reporter.getOutput(result);
                for (String output : outputList) {
                    //将用例的log输出报告中
                    test.debug(output);
                }
                if (result.getThrowable() != null) {
                    test.log(status, result.getThrowable());
                } else {
                    test.log(status, "Test " + status.toString().toLowerCase() + "ed");
                }
                //test.getModel().setStartTime(getTime(result.getStartMillis()));
                //test.getModel().setEndTime(getTime(result.getEndMillis()));
            }
        }
    }

    private Date getTime(long millis) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(millis);
        return calendar.getTime();
    }

}

在xml文件中设置监听

       <listener class-name="com.frame.demo.report.ExtentTestngReporterListener"/>

运行测试后,会自动生成测试报告,如下:

到此测试报告部分介绍完毕,现在整体的一个测试框架就算搭建完成了!!

整体运行效果:

总结

笔者仅是提供一个思路,希望感兴趣的同学可以从头到下自己敲一遍,笔者能力有限,还请勿喷,如果大家有更好的想法,还请在文末给我留言,一起学习交流!!

友情提示

原创不易,如转载或复制以上代码还请注明出处,觉得文章好,还请添加关注!

猜你喜欢

转载自www.cnblogs.com/longronglang/p/11400742.html