点我达自动化的从0到1系列之WebUI(一)

臧朝鹏

概述

  上图是逐轮演变最终成型的目前点我达自动化测试的整体结构,一如既往的秉承了分层思想,各项目以微服务形式相对独立,又为其他需求方提供服务。最底层的DWD-Test-common公共资源jar包主要用来提供mybatis数据操作,公用的业务及非业务的枚举类、DTO对象,以及各种工具util。而request-service服务主要给其他测试项目提供测试服务支撑,包括前置数据构建,测试数据清洗,业务api接口管理等。

  webUI-test项目是我们目前五个自动化测试项目中的一个,处于UI测试,接口测试,moke测试三层自动化测试结构的最顶层。是我们测试团队最早开始并还在持续优化的一个测试项目。该项目主要用来保障我们hawkeye、CRM后台的基本可用性。

  本项目基于Selenium WebDriver开源框架,并且在项目中引入了springboot,durid,dubbo,testng。各个部分在项目中承担的作用如下图所示:

特点

一、以服务的形式个性化执行测试用例

  为了拓展用例执行的灵活性,我们给测试项目设计了controller层,主要实现测试用例的扫描和测试用例的执行。即扫描工程中的@Test注解、注解中的testName和description属性值,以及其他用例相关信息,将这些数据进行组装,返回给质量平台使用。通过质量管理平台前端页面我们可以根据个性化需求制定要执行的测试用例集。webUI测试项目则通过testng的XmlSuite类创建一个虚拟的testng.xml文件执行测试。代码如下

/**
 * 扫描测试用例
 * @return 返回测试用例集合
 */
public static TestCaseDTO getTestCase(String packageName, String testCaseName) throws Exception {  
    TestCaseDTO testCaseDTO = new TestCaseDTO();
    List<TestModuleDTO> testModuleDTOList = new ArrayList<>();
    //获取指定包路径下的模块名称
    List<String> moduleList = PackageUtil.getModelName(packageName);
    if (moduleList == null) {
        System.out.println( "包路径为:"+packageName);
        System.out.println( "\n包路径下没有测试用例!");
        return null;
    }
    for (String moduleName : moduleList){
        if(moduleName.contains(".class")){continue;}
        String modulepath="";
        if (moduleName.contains(".")){
            modulepath = moduleName;
        }else{
            modulepath = packageName+"."+moduleName;
        }
        //获取指定模块路径下的测试类名称
        Set<String> classnames = PackageUtil.getClassName(modulepath,true);
        List<TestClassDTO> testClassDTOList = new ArrayList<>();
        for (String classname : classnames) {
            TestClassDTO testClassDTO = new TestClassDTO();
            //将Sting类型类名称转换为class
            Class<?> clazz = Class.forName(classname).newInstance().getClass();  //clazz是injection
            //通过对类的注释进行比对筛选出目标类
            Annotation[] classAnnotations = clazz.getAnnotations();
            String interfacename = "";
            for (Annotation classAnnotation : classAnnotations) {
                if(classAnnotation instanceof Test){
                    interfacename = ((Test) classAnnotation).description();
                }
                if (classAnnotation instanceof Listeners){
                    System.out.println("测试类:"+classname);
                    //获取测试类下的所有方法
                    Method[] methods = clazz.getMethods();
                    List<TestMethodDTO> testMethodList = new ArrayList<>();
                    for (Method method : methods) {
                        //通过对方法的注释进行比对筛选出目标测试方法
                        Annotation[] methodAnnotations = method.getAnnotations();
                        for (Annotation methodAnnotation : methodAnnotations) {
                            if (methodAnnotation instanceof Test) {
                                TestMethodDTO testMethodDTO = new TestMethodDTO();
                                System.out.println("测试方法:"+method.getName());
                                String beanName = ((Test) methodAnnotation).testName();
                                System.out.println("测试方法的名称:"+beanName);
                                testMethodDTO.setTestMethodName(method.getName());
                                testMethodDTO.setTestName(beanName);
                                testMethodList.add(testMethodDTO);
                            }
                        }
                    }
                    testClassDTO.setTestClassName(classname);
                    testClassDTO.setTestInterface(interfacename);
                    testClassDTO.setTestMetchodList(testMethodList);
                    testClassDTOList.add(testClassDTO);
                }
            }
        }
        TestModuleDTO testModuleDTO = new TestModuleDTO();
        testModuleDTO.setTestModelName(moduleName);
        testModuleDTO.setTestClassDTOList(testClassDTOList);
        testModuleDTOList.add(testModuleDTO);
    }
    testCaseDTO.setTestCaseName(testCaseName);
    testCaseDTO.setTestModuleDTOList(testModuleDTOList);
    return testCaseDTO;
}
/**
 * 创建虚拟testng.xml文件并执行
 * @param testCaseDTO  测试用例集合
 * @return 返回虚拟测试套件
 */
public static XmlSuite creatXmlSuite(TestCaseDTO testCaseDTO){  
    //初始化配置文件
    XmlSuite suite = new XmlSuite();
    suite.setName(testCaseDTO.getTestCaseName());
    suite.setVerbose(1);
    suite.setThreadCount(1);

    //设置配置文件参数
    Map<String, String> parameters = new HashMap<>();
    parameters.put("testCaseName", testCaseDTO.getTestCaseName());
    parameters.put("ccEmail", testCaseDTO.getCcEmail());
    parameters.put("receiveEmail", testCaseDTO.getReceiveEmail());
    suite.setParameters(parameters);
    List<String> listeners = new ArrayList<>();
    listeners.add("com.dwd.test.util.extentreports.ExtentTestNGIReporterListener");
    listeners.add("com.dwd.test.util.testng.NewAssertListener");
    suite.setListeners(listeners);

    //需要排除的测试类
    XmlTest test = new XmlTest(suite);
    test.setName(testCaseDTO.getTestCaseName());
    test.setExcludedGroups(Arrays.asList(new String[]{"excludedGroup"}));

    //循环加载测试类
    List<XmlClass> classes  = new ArrayList<>();
    List<TestModuleDTO> testModuleDTOList = testCaseDTO.getTestModuleDTOList();
    for (TestModuleDTO testModuleDTO:testModuleDTOList){
        List<TestClassDTO> testClassDTOList = testModuleDTO.getTestClassDTOList();
        for (TestClassDTO testClassDTO : testClassDTOList) {
            String className = testClassDTO.getTestClassName();
            List<XmlInclude> xmlIncludes = new ArrayList<>();
            for (TestMethodDTO testMethodDTO: testClassDTO.getTestMetchodList()) {
                XmlInclude xmlInclude = new XmlInclude(testMethodDTO.getTestMethodName());
                xmlIncludes.add(xmlInclude);
            }
            XmlClass xmlClass = new XmlClass(className);
            xmlClass.setIncludedMethods(xmlIncludes);
            classes.add(xmlClass);
        }
    }
    test.setXmlClasses(classes);
    System.out.println("创建的虚拟testng.xml为:"+suite.toString());
    return suite;
}
二、跨环境、跨数据库执行测试用例

  一开始我们只能在单一系统进行持续集成测试,后来借测试环境上云的契机,我们对测试系统进行了跨环境、跨数据库、跨版本(接口版本)的升级改造。我们把测试数据拆分为环境差异数据(例如一些账号)和非环境差异数据。通过获取系统参数判断当前自动化测试项目所运行的物理环境,从而进行测试数据的路由。 获取系统参数的代码如下:

  获取当前测试环境信息后,我们就能轻松的实现测试用例测试数据的路由关系,来区分不同的测试环境。代码如下:

public class BaseDataProvider {

    private static String MASTER_ENV = "master";
    private static String BRANCH_ENV = "branch";

    public static String env(){
        return EnvUtil.isBranchEnv()?BRANCH_ENV:MASTER_ENV;
    }
}
public class FortuneV4Provider extends BaseDataProvider {  
    //商家收费
    @DataProvider(name = "shopFortuneV4")
    public static Iterator<Object[]> shopFortuneV4(Method method) throws IOException {
        return new ExcelDataProvider("webui/testdata/exceldata/" + env() + "/fortuneV4/shopFortuneV4");
    }
}
三、提供便捷的前置条件构造接口

  另外,我们还对WEBUI自动化测试项目搭建了dubbo服务,对于部分需要前置条件的功能,我们可以通过request-service提供的dubbo接口进行前置条件构建,从而满足被测功能测测试条件。

四、采用pageObject思想进行封装

  和大多数webUI自动化测试项目一样我们也采用了pageObject思想将页面元素定位进行了封装,根据我们自动化测试脚本开发规范制订了ById、ByName、ByCssSelector、ByXpath的优先级顺序。并对重复的操作步骤进行了action提炼,提高代码的复用率及测试脚本的可读性。代码如下:

public class ShopMenuPage {  
    private WebDriver driver;
    public ShopMenuPage(WebDriver driver) {
        this.driver = driver;
    }
    /**定位商家名输入框*/
    public WebElement getShopName() {
        By by = By.id("shopName");
        return BaseCase.isElementExist(by) ? driver.findElement(by) : null;
    }
}
public class FortuneV4Action extends BaseAction{  
    //计费V4页面元素
    private FortuneV4Page allFortuneV4Page;
    //用于等待页面元素加载出来
    private WebDriverWait wait;

    public FortuneV4Action(WebDriver driver , FortuneV4Page allFortuneV4Page){
        super(driver);
        this.allFortuneV4Page = allFortuneV4Page;
    }

    //点击商家详情-"计费V4"
    public void clickFortuneV4(WebDriver driver){
        click(allFortuneV4Page.getFortuneV4());
        //等待结果加载出来
        wait = new WebDriverWait(driver , 10);
        wait.until(ExpectedConditions.visibilityOfElementLocated(allFortuneV4Page.getShopFortuneModalBy()));
    }
}
五、设计了页面直达用例设计思想

  为了节约测试时间,我们提炼了页面直达思想,通过请求预制的URL地址,可以跳过一系列中间过程的菜单操作,直达一些常用的目标页面进行下一步操作。代码如下:

@Component
public class ShopDetailUrl extends BaseProperties{  
    //商家清单地址
    public String crmShopDetailURL(String shopId){
        return "http://"+shopservice+"/index.html#/ShopDetail/"+shopId+"/1";
    }
}
六、完整断言校验监听设计

  有了上面的测试框架支撑,我们就可以顺利的进行测试用例的组织。

@Listeners({NewAssertListener.class})
@Component
public class CancelWorkorderTest extends BaseCase {

    @Resource
    private WorkOrderMapper workOrderMapper;

    @Test(dataProvider = "cancelWorkOrder",dataProviderClass = AllWorkorderProvider.class,testName = "工单取消测试用例",description = "适用于新增工单的取消")
    public void testCancelWorkOrder(Map<String,String> data) throws InterruptedException {
        //登陆
        LoginAction loginAction=new LoginAction(driver) ;
        loginAction.Login(data,loginUrl.crmLoginURL(),driver);
        //点击菜单
        ClickMenuAction clickMenuAction=new ClickMenuAction(driver);
        clickMenuAction.setMenu(driver);
        //新增工单
        NewWorkOrderAction newWorkOrderAction =new NewWorkOrderAction(driver);
        newWorkOrderAction.setWorkInsertConditon(data,driver);
        newWorkOrderAction.setWorkCancel(data,driver);
        //根据新增的serviceId进行校验
        String serviceIDFormExcel=data.get("serviceId");
        //查询数据库
        WorkOrder workOrder =workOrderMapper.getWorkorderBysort();
        String serviceIdFromDB=workOrder.getServiceId();
        //excel中的serviceId和数据库中最新的serviceId比较,不相同表示没有新增,取消成功
        NewAssert.assertEquals(serviceIDFormExcel,serviceIdFromDB,"取消失败,新生成的工单号依然存在");
    }
}

  从上面的测试用例中可以看到,我们给每个测试类都添加了一个自己封装的NewAssertListener的注解用来收集测试用例的执行结果。当一个测试用例有多个断言时,并不会由于某个断言失败而导致整个用例结束,而是将整个用例结束后统一抛出错误断言信息,使测试更加完整。

七、集成extentreports测试报告插件

  由于testng自带的测试报告样式过于陈旧,不能满足我们紧跟时代步伐的测试宝宝们的审美要求,所以我们项目集成了extentreports测试报告插件,在测试套件执行完成后,根据测试结果组装成对应的测试报告,并用邮件发出。

未来

  未来我们整个自动化测试体系将和我们正在研发中的质量管理平台做深度的融合,使它不仅仅作为持续集成测试,还将深入的参与到日常项目测试流程中,并且每次的测试数据作为项目上线的重要参考依据。

PS:我们测试开发组现在招贤纳士,有一定前后端开发经验、自动化测试经验、接口测试经验的小哥哥,小姐姐,欢迎投递简历到zangchaopeng@dianwoda.com,期待能够与你一起工作。