技术部突然宣布:JAVA开发人员全部要会接口自动化测试框架

14,322 阅读49分钟

整理了一些Java方面的架构、面试资料(微服务、集群、分布式、中间件等),有需要的小伙伴可以关注公众号【程序员内点事】,无套路自行领取

写在前边

用单元测试Junit完全可以满足日常开发自测,为什么还要学习TestNG,都影响了我的开发进度!

最近技术部老大突然宣布:全体开发人员必须熟练掌握自动化测试框架TestNG,就有了上边同事们的抱怨,是的,开始我也在抱怨,因为并不知道它是个什么东东,但从开始接触到慢慢编写测试用例,应用到项目后,我发现它真的超实用。

我们来一起看看它比Junit好在哪?


一、TestNG初识

TestNG[后面都简称为TG]是一款为了大量测试(比如测试时多接口数据依赖)需要,所诞生的一款测试框架,从简单的单元测试再到集成测试甚至是框架级别的测试,都可以覆盖到,因此是一款非常强大的测试框架!

1.编写步骤

常规的TG的测试案例有三个步骤

  1. 编写业务测试代码时,插入TG提供的注解

  2. 将要测试的类、group等进行编排,写入到testng.xml或build.xml

  3. 运行单个TG测试案例【idea、eclipse、ant、cmd等都可以运行】

  4. 运行整个项目的test-ng测试案例【如果是多模块项目,则是进入到对应的模块目录运行命令或者配置】

    • xml

      <plugin>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
      	<groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
      	<configuration>
      		<skip>false</skip>
      		<testFailureIgnore>false</testFailureIgnore>
      		<suiteXmlFiles>
      			<file>${project.basedir}/src/test/OrderTest.xml</file>
      		</suiteXmlFiles>
      	</configuration>
      </plugin>
      
    • MVN命令

      此处的命令会优先于pom.xml中的配置,相对于java命令更优,因为mvn处理打包的一环,方便监控

      mvn clean package/install -DskipTests
      
      mvn clean package/install -Dmaven.test.skip=true
      #或者直接执行
      mvn test
      

2.相关注解

注解 作用
@BeforeSuite 被注解的方法,将在整个测试套件之前运行
@AfterSuite 被注解的方法,将在整个测试套件之后运行
@BeforeTest 被注解的方法,将在测试套件内所有用例执行之前运行
@AfterTest 被注解的方法,将在测试套件内所有用例执行之后运行
@BeforeGroups 被注解的方法,将在指定组内任意用例执行之前运行
@AfterGroups 被注解的方法,将在指定组内任意用例执行之后运行
@BeforeClass 被注解的方法,将在此方法对应类中的任意其他的,被标注为@Test 的方法执行前运行
@AfterClass 被注解的方法,将在此方法对应类中的任意其他的,被标注为@Test 的方法执行后运行
@BeforeMethod 被注解的方法,将在此方法对应类中的任意其他的,被标注为@Test的方法执行前运行
@AfterMethod 被注解的方法,将在此方法对应类中的任意其他的,被标注为@Test的方法执行后运行
@DataProvider 被注解的方法,强制返回一个 二维数组Object[ ][ ]作为另外一个@Test方法的数据工厂
@Factory 被注解的方法,作为对象工厂,强制返回一个对象数组 Object[ ]
@Listeners 定义一个测试类的监听器
@Parameters 定义一组参数,在方法运行期间向方法传递参数的值,参数的值在testng.xml中定义
@Test 标记方法为测试方法,如果标记的是类,则此类中所有的public方法都为测试方法

备注:相关对应的属性配置值,点进去对应类中查询即可,不在一一赘述

3.名词解释

suite

​ 由xml文件表示,包含一个或者多个测试案例,使用标签包裹

​ 一般来讲,一个xml的<suite>对应一个java类,除非特殊情况,在java中需要特别指定<suite>

​ 否则xml对应java类的所有@Test注解属性suiteName默认都是xml中定义的<suite name='xxx'>

test

​ 由标签表示,包含一个或者多个TestNG的类

1.Check-in tests 登记类测试

这些测试类需要在提交新代码前运行,保证基本功能不会被破坏

2.Functional tests 功能类测试

这些测试应该覆盖软件的所有功能,并且每天至少运行一次,即使有些情况下你不想运行它

check-in test是Functional tests的子集

基础示例
public class Test1 {
  @Test(groups = { "functest", "checkintest" })
  public void testMethod1() {
  }
 
  @Test(groups = {"functest", "checkintest"} )
  public void testMethod2() {
  }
 
  @Test(groups = { "functest" })
  public void testMethod3() {
  }
}
<test name="Test1">
  <groups>
    <run>
      <include name="functest"/>
      <!-- <include name="checkintest"/> -->
    </run>
  </groups>
  <classes>
    <class name="example1.Test1"/>
  </classes>
</test>

运行testng.xml文件结果:

所有@Test注解中,在xml中定义的1、2、3方法都会被运行,如果换成check-intest,则只运行方法1、2

总结:

1.xml文件定义测试案例的运行策略

2.所谓的测试案例类别,只是概念上的定义

​ --- 登记类的测试案例,如果不在xml编排中没有涉及,它不一定运行

​ --- 功能性测试类,是我们在xml编排中,一定会运行的测试案例

扩展示例
1.正则匹配
@Test
public class Test1 {
  @Test(groups = { "windows.checkintest" })
  public void testWindowsOnly() {
  }
 
  @Test(groups = {"linux.checkintest"} )
  public void testLinuxOnly() {
  }
 
  @Test(groups = { "windows.functest" )
  public void testWindowsToo() {
  }
}
<test name="Test1">
  <groups>
    <run>
      <include name="windows.*"/>
    </run>
  </groups>
 
  <classes>
    <class name="example1.Test1"/>
  </classes>
</test>

大家发挥一下脑洞,应该可以猜到运行结果【不要被windows的命名所引诱】

2.正则排除

【官方原话,不建议使用此类写法】

如果您开始重构Java代码(标记中使用的正则表达式可能与您的方法不再匹配)

这会使您的测试框架很可能崩溃

package org.vk.test.springtest_testng;

import org.testng.annotations.Test;

public class Test1 {

    @Test(groups = {"functest", "checkintest"})
    public void testMethod1() {
        System.out.println(1);
    }

    @Test(groups = {"functest", "checkintest"})
    public void testMethod2() {
        System.out.println(2);
    }

    @Test(groups = {"functest"})
    public void testMethod3() {
        System.out.println(3);
    }
}
<suite name="Suite" parallel="classes" thread-count="1">
    <test name="Test1">
        <groups>
            <run>
                <include name="functest"/>
            </run>
        </groups>
        <classes>
            <class name="org.vk.test.springtest_testng.Test1">
                <methods>
                    <include name="testMethod*"></include>
                    <exclude name="testMethod3"></exclude>
                </methods>
            </class>
        </classes>
    </test>
</suite>

运行结果:testMethod3不会执行

总结:suit配置,从上而下,其实是对编排规则的一个层层过滤

对应group配置,显然所有方法都会执行,但是到了class配置时,对其再次配置,过滤了方法3

test class

​ java类,包含至少一个TG的注解,由表示

test method

​ java方法,含有@Test注解,默认情况下,test方法的返回值都会忽略,除非声明需要返回

<suite allow-return-values="true">
<!--或者-->
<test allow-return-values="true">

test group

​ 测试方法组,不仅可以定义方法属于哪个group,还可以设置group包含哪些子group,TG会自动调用

​ 可以在testng.xml的<test>or<suite>中定义

​ 如果在中指定组“a”,在中指定组“b”,则“a”和“b”都将包括在内

示例1:基础

@Test(groups = {"checkintest", "broken"} )
public void testMethod2() {
}
<test name="Simple example">
  <groups>
    <run>
      <include name="checkintest"/>
      <exclude name="broken"/>
    </run>
  </groups>
  
  <classes>
    <class name="example1.Test1"/>
  </classes>
</test>

运行结果:什么都没有

总结:xml配置,决定最终结果,无论出现什么反思维的配置,理论上什么结果就是最终结果

【官方提示】

达到禁用效果,也可以通过使用@Test和@Before/After注释上的“enabled”属性单独禁用测试。

示例2:组合

@Test(groups = { "checkin-test" })
public class All {
 
  @Test(groups = { "func-test" )
  public void method1() { ... }
 
  public void method2() { ... }
}

结果:method1属于checkin-test和func-test两个组,method2仅属于checkin-test组

groups of groups

​ group里面含有子group,称为 MetaGroups,我自己称为元组

functest和checkintest在名词解释的test小节中有提到,此处我们将其funtest再细化分windows、linux组

新增all组,包含两个大组

<suite name="Suite" parallel="classes" thread-count="1">    
	<test name="Test1">
      <groups>
        <define name="functest">
          <include name="windows"/>
          <include name="linux"/>
        </define>

        <define name="all">
          <include name="functest"/>
          <include name="checkintest"/>
        </define>

        <run>
          <include name="all"/>
        </run>
      </groups>

      <classes>
        <class name="example1.Test1"/>
      </classes>
    </test>
</suite>

结果:所有方法都会运行

总结:可以对多个测试进行组合编排,形成group表示一个大的功能

testng.xml

​ testng.xml的每个section部分,都可以在ant、命令行对应的文档中找到【另外2种调用TG的方式】

个人理解:

一般来讲,直接在idea、eclipse中运行@Test类也可以,但为什么我们需要testng.xml?

原因:如果需要对java类、方法的测试案例进行编排,不使用xml进行编排,仅仅提供xml文件以外的方式,很难做到高度灵活的业务逻辑测试!

parameters

测试案例的参数,测试方法的入参配置,执行方法时用到

测试案例的参数

1.@Parameters
1.1 xml注入
@Parameters({ "first-name" })
@Test
public void testSingleString(String firstName) {
  System.out.println("Invoked testString " + firstName);
  assert "Cedric".equals(firstName);
}
<suite name="My suite">
  <parameter name="first-name"  value="Cedric"/>
  <test name="Simple example">
  <-- ... -->
</suite>
  1. XML参数映射到Java参数的顺序与注释中的顺序相同,如果数量不匹配,TestNG将发出一个错误。

  2. 参数的作用域:

    在testng.xml中,可以在标记下或下声明它们。

    如果两个参数具有相同的名称,则在中定义的参数具有优先权。如果您需要指定一个适用于所有测试的参数,并仅对某些测试重写其值,则这很方便。

1.2 @Optional注入
@Parameters("hello")
@Test(groups = {"functest"})
public void testMethod4(@Optional("hello") String hello) {
	System.out.println(hello);
}

【官方说明】

@Parameters注解,同样适用于 @Before/Afterand @Factory此类的注解!结果:输出hello字符串

总结:以上的方式,适合简单参数配置,不适合做复杂对象注入

2.@DataProvider

这种方式是为了弥补第一种方式而衍生的,如果参数构建比较复杂,复杂对象无法在xml或者利用@Optional注解

构建时,就需要这种方式了

示例:同一个类
//This method will provide data to any test method that declares that its Data Provider
//is named "test1"
@DataProvider(name = "test1",parallel = true)
public Object[][] createData1() {
 return new Object[][] {
   { "Cedric", new Integer(36) },
   { "Anne", new Integer(37)},
 };
}
 
//This test method declares that its data should be supplied by the Data Provider
//named "test1"
@Test(dataProvider = "test1")
public void verifyData1(String n1, Integer n2) {
 System.out.println(n1 + " " + n2);
}

输出结果:

Cedric 36
Anne 37

备注:并发线程数设置可以在xml的<suite data-provider-thread-count="20">中调整,默认配置10个线程

​ 如果要在不同的线程池中运行一些特定的数据提供程序,则需要从不同的XML文件运行它们。

示例:跨类
public class StaticProvider {
  @DataProvider(name = "create")
  public static Object[][] createData() {
    return new Object[][] {
      new Object[] { new Integer(42) }
    };
  }
}
 
public class MyTest {
  @Test(dataProvider = "create", dataProviderClass = StaticProvider.class)
  public void test(Integer n) {
    // ...
  }
}
示例:其他类
@DataProvider(name = "test1")
public MyCustomData[] createData() {
  return new MyCustomData[]{ new MyCustomData() };
}
@DataProvider(name = "test1")
public Iterator<MyCustomData> createData() {
  return Arrays.asList(new MyCustomData()).iterator();
}
@DataProvider(name = "test1")
public Iterator<Stream> createData() {
  return Arrays.asList(Stream.of("a", "b", "c")).iterator();
}
参数返回值说明
  • Object[] []

    数组第一个维度的大小是调用测试方法的次数

    数组第二个维度的大小包含必须与测试方法的参数类型兼容的对象数组

  • Iterator<Object[ ]>

    和第一种返回类型不同,这种方式允许你对返回值做懒初始化

    唯一的限制是在迭代器的情况下,它的参数类型不能被显式地参数化

    如果有很多参数集要传递给方法,并且不想预先创建所有参数集,那么这一点特别有用

下面这几个例子都有一个特性: can't be explicitly parametrized

@DataProvider(name = "test1")

不允许显示的初始化,有使用经验的人可以私聊!目前我这边直接把代码粘上去,是会报错的。

factory

工厂还可以与数据提供程序一起使用,可以通过将@Factory注释放在常规方法或构造函数上来利用此功能

使用@Factory可动态地创建测试,一般用来创建一个测试类的多个实例,每个实例中的所有测试用例都会被执行,@Factory构造实例的方法必须返回Object[]。

下一个小节的dependencyes,就有对其应用,此处不做过多的说明了就,官网示例和其相差不大。

dependencyes

某些情况,我们需要对执行顺序做编排,TG提供了2种方式:

  • xml

    <test name="My suite">
      <groups>
        <dependencies>
          <group name="c" depends-on="a  b" />
          <group name="z" depends-on="c" />
        </dependencies>
      </groups>
    </test>
    
  • 注解

    @Test 注解上设置依赖的属性: dependsOnMethods 或者 dependsOnGroups

硬性依赖

依赖的所有方法都必须已运行并成功才能运行。

如果依赖项中至少发生一个错误,则不会在报表中调用并标记为跳过

默认情况下alwaysRun=false

@Test(groups = { "init" })
public void serverStartedOk() {}
 
@Test(groups = { "init" })
public void initEnvironment() {}
 
@Test(dependsOnGroups = { "init.*" })
public void method1() {}

如果依赖的方法失败,并且对它有硬依赖关系,则依赖它的方法不会标记为失败,而是标记为跳过。跳过的方法将在最终报告中以同样的方式报告(在HTML中,颜色既不是红色也不是绿色),这一点很重要,因为跳过的方法不一定是失败的。

高级用法参照【 beust.com/weblog/2004…

软性依赖

即使有些方法失败了,你也会一直在追求你所依赖的方法。

当您只想确保您的测试方法以特定的顺序运行,但它们的成功并不真正依赖于其他方法的成功时,这非常有用。

通过在@Test注释中添加alwaysRun=true获得软依赖性。

group-by-instances

测试案例按照实例进行分组运行

通常的dependsOnGroups依赖注解,只能实现以下的模式:

a(1)
a(2)
b(2)
b(2)

但是也有一种情况, 假设我们要实现多组用户一组操作行为:登录、登出

signIn("us")
signOut("us")
signIn("uk")
signOut("uk")

此时我们需要使用@Factory注解,配合group-by-instance来实现

Test1.java

public class Test1 {

   private String countryName;

    public Test1(String countryName) {
        this.countryName = countryName;
    }

    @Test
    public void signIn() {
        System.out.println(countryName + " signIn");
    }

    @Test(dependsOnMethods = "signIn")
    public void signOut() {
        System.out.println(countryName + " signOut");
    }

}

Test.xml

<suite name="Suite">
    <test name="Test1" >
        <classes>
            <class name="org.vk.test.springtest_testng.Test1"></class>
        </classes>
    </test>
</suite>

TestFactory.java

public class TestFactory {

    @Factory(dataProvider = "init")
    public Object[] test(int nums) {
        Object[] object = new Object[nums];
        List<String> ctrys = Arrays.asList("US", "UK", "HK");
        for (int i = 0; i < nums; i++) {
            Test1 t = new Test1(ctrys.get(i));
            object[i] = t;
        }
        return object;
    }

    @DataProvider//可缺省名称,默认以方法名为准
    public Object[][] init() {
        return new Object[][]{new Object[]{3}};
    }
}

TestFactory.xml

<suite name="Suite2" group-by-instances="true">
    <test name="TestFactory" >
        <classes>
            <class name="org.vk.test.springtest_testng.TestFactory"/>
        </classes>
    </test>
</suite>

verbose

verbose="2" 标识的就是记录的日志级别,共有0-10的级别,其中0表示无,10表示最详细

preserve-order

默认就是true,<test>标签下的class按顺序执行

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Preserve order test runs">
  <test name="Regression 1" preserve-order="true">
    <classes>
      <class name="com.pack.preserve.ClassOne"/>
      <class name="com.pack.preserve.ClassTwo"/>
      <class name="com.pack.preserve.ClassThree"/>
    </classes>
  </test>
</suite>

4.并发单元测试

如果您运行多个套件文件(例如“java org.testng.testng testng1.xml testng2.xml”),并且希望这些套件在单独的线程中运行,那么这非常有用。可以使用以下命令行标志指定线程池的大小:

1.cmd命令

java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.xml

2.xml配置

<suite name="My suite" parallel="methods" thread-count="5"></suite>
<!--TestNG将在单独的线程中运行所有的测试方法。依赖方法也将在单独的线程中运行,但它们将遵循您指定的顺序-->
<suite name="My suite" parallel="tests" thread-count="5"></suite>
<!--TestNG将在同一线程中的同一个<test>标记中运行所有方法,但每个<test>标记将在单独的线程中。这允许您将所有非线程安全的类分组到同一个<test>中,并保证它们都将在同一个线程中运行,同时利用TestNG使用尽可能多的线程来运行测试。-->
<suite name="My suite" parallel="classes" thread-count="5"></suite>
<!--TestNG将在同一线程中运行同一类中的所有方法,但每个类将在单独的线程中运行。-->
<suite name="My suite" parallel="instances" thread-count="5"></suite>
<!--TestNG将在同一线程中运行同一实例中的所有方法,但两个不同实例上的两个方法将在不同线程中运行。-->

3.注解配置

从三个不同的线程调用函数testServer十次。10秒的超时保证没有一个线程会永远阻塞这个线程。

@Test(threadPoolSize = 3, invocationCount = 10, timeOut = 10000)//timeOut不管是否多线程都有效
public void testServer() {
	...
	...
}

5.失败的测试

如何找到

每次在套件中测试失败时,TestNG都会在输出目录中创建一个名为TestNG-failed.xml的文件。

这个XML文件包含了只重新运行失败的方法所必需的信息,允许您快速地重新生成失败,而不必运行整个测试。

因此,典型会话如下所示:

java -classpath testng.jar;%CLASSPATH% org.testng.TestNG -d test-outputs testng.xml
java -classpath testng.jar;%CLASSPATH% org.testng.TestNG -d test-outputs test-outputs\testng-failed.xml

testng-failed.xml将包含所有必需的依赖方法,这样您就可以保证在没有任何跳过失败的情况下运行失败的方法。

其他方式:通过测试报告target\surefire-reports\index.html或者第三方测试报告插件也可以获取

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UCKF794W-1581562336476)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1579156460298.png)]

如何重试

如果测试案例出现错误,想要启用TG的重试步骤如下:

  1. 构建一个java类,实现org.testng.IRetryAnalyzer接口
  2. 将第一步构建的类绑定到@Test注释上,例如@Test(retryAnalyzer=LocalRetry.class)
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
 
public class MyRetry implements IRetryAnalyzer {
 
  private int retryCount = 0;
  private static final int maxRetryCount = 3;
 
  @Override
  public boolean retry(ITestResult result) {
    if (retryCount < maxRetryCount) {
      retryCount++;
      return true;
    }
    return false;
  }
}
import org.testng.Assert;
import org.testng.annotations.Test;
 
public class TestclassSample {
 
  @Test(retryAnalyzer = MyRetry.class)
  public void test2() {
    Assert.fail();
  }
}

6.执行方式

1.命令行

java org.testng.TestNG testng1.xml [testng2.xml testng3.xml ...
选项 参数类型 说明
-configfailurepolicy skip continue
-d 一个目录 生成测试报告的地址
-dataproviderthreadcount 并行运行测试案例的默认线程数 并行测试时,设置默认的最大线程数,前提是使用【-parallel】选项才会生效
-excludegroups 逗号分割的组列表 排除需要运行的组列表
-groups 逗号分割的组列表 需要运行的组列表 (示例. "windows,linux,regression").
-listener classpath目录下能找到的java类 允许自己定义测试监听器,但必须要实现org.testng.ITestListener
-usedefaultlisteners true false
-methods 逗号分割的全路径类方法 指定特定的方法运行,com.OBJ1.test,com.Obj2.test
-methodselectors 逗号分割的方法优先级列表 指定方法选择器,com.Selector1:3,com.Selector2:2
-parallel methods|tests|classes 设置默认测试的并行线程数。如果未设置,默认机制是单线程测试。这可以在套件定义中重写。可以是方法、测试案例、类
-reporter 自定义报表监听器 -listener 选项功能相似,只是它允许在报告中额外设置JavaBeans的属性 Example: -reporter com.MyReporter:methodFilter=*insert*,enableFiltering=true 可以出现一次或者多次,如果有必要的话
-sourcedir 逗号分割的目录 JavaDoc注释的测试源所在的目录。只有在使用JavaDoc类型注释时,此选项才是必需的. "src/test" or "src/test/org/testng/eclipse-plugin;src/test/org/testng/testng"
-suitename 默认套件suit名称 如果suit.xml或者源码配置了相关名称,则忽略此配置
-testclass classpath目录下,逗号分割的java类列表 "org.foo.Test1,org.foo.test2"
-testjar jar包名称 指定包含测试类的jar文件。如果在该jar文件的根目录下找到testng.xml文件,则将使用该文件,否则,在该jar文件中找到的所有测试类都将被视为测试类。
-testname 测试案例的默认名称 指定在命令行上定义的测试的名称。如果suite.xml文件或源代码指定了不同的测试名称,则忽略此选项。如果用双引号“like this”将测试名称括起来,则有可能创建一个包含空格的测试名称。
-testnames 逗号分割的测试名称 只有测试案例的 匹配上此处的配置才会运行
-testrunfactory 逗号分隔的classpath下可以找到的java类 允许自己定义要运行的类. 类必须要实现org.testng.ITestRunnerFactory
-threadcount 数字 设置并发运行测试案例的最大线程数.只有使用-parallel选项才会生效 如果suit中有定义,则该配置会被忽略/覆盖。
-xmlpathinjar jar包下xml的路径 包含测试jar中有效XML文件的路径(例如“resources/testng.XML”)。默认值是“testng.xml”,这意味着在jar文件的根目录下有一个名为“testng.xml”的文件。除非指定了“-testjar”,否则将忽略此选项。

2.ant

testng.org/doc/ant.htm…

3.eclipse

testng.org/doc/eclipse…

4.idea

testng.org/doc/idea.ht…

  • Package: 指定一个package运行.包下的测试案例都会运行.
  • Group: 指定一个TestNG group运行.
  • Suite: 指定一个testng.xml 文件运行
  • Class: 运行对应类中的所有测试案例.
  • Method: 运行单个方法的测试案例.

7.扩展模块

编程式测试

本例创建一个TestNG对象并运行测试类Run2。

它还添加了一个TestListener。您可以使用适配器类org.testng.TestListenerAdapter,也可以自己实现org.testng.ITestListener。此接口包含各种回调方法,可用于跟踪测试何时开始、成功、失败等。

TestListenerAdapter tla = new TestListenerAdapter();
TestNG testng = new TestNG();
testng.setTestClasses(new Class[] { Run2.class });
testng.addListener(tla);
testng.run();

再比如如果想实现类似于xml这样的功能:

<suite name="TmpSuite" >
  <test name="TmpTest" >
    <classes>
      <class name="test.failures.Child"  />
    <classes>
    </test>
</suite>

那么你可以这样编程:

// 1.编排
XmlSuite suite = new XmlSuite();
suite.setName("TmpSuite");
 
XmlTest test = new XmlTest(suite);
test.setName("TmpTest");
List<XmlClass> classes = new ArrayList<XmlClass>();
classes.add(new XmlClass("test.failures.Child"));
test.setXmlClasses(classes) ;
// 2.运行
List<XmlSuite> suites = new ArrayList<XmlSuite>();
suites.add(suite);
TestNG tng = new TestNG();
tng.setXmlSuites(suites);
tng.run();

jitpack.io/com/github/…

BeanShell

如果testng.xml中的和标记不足以满足您的需要,您可以使用BeanShell表达式来决定某个测试方法是否应包含在测试运行中。

您可以在标记下指定此表达式:

<test name="BeanShell test">
   <method-selectors>
     <method-selector>
       <script language="beanshell"><![CDATA[
         groups.containsKey("test1")
       ]]></script>
     </method-selector>
   </method-selectors>
  <!-- ... -->

当在testng.xml中找到script标记时,testng将忽略当前标记中组和方法的后续和,您的BeanShell表达式将是决定是否包含测试方法的唯一方法。

另外有几个地方还需要注意:

它必须返回布尔值。除此约束外,允许任何有效的BeanShell代码(例如,您可能希望在工作日期间返回true,而在周末期间返回false,这将允许您根据日期以不同的方式运行测试)。

  • 为了方便编写BeanShell条件,TG准备了以下几个参数:

    java.lang.reflect.Method --- method: the current test method. org.testng.ITestNGMethod --- testngMethod: the description of the current test method. java.util.Map groups---: a map of the groups the current test method belongs to.

  • CDATA声明(如上所示)将表达式包围起来,以避免冗长地引用保留的XML字符。

注解转换器

TestNG允许您在运行时修改所有注释的内容,如果希望在运行时重写特定注解,需要使用到注解转换器.

实践步骤:

1.实现 IAnnotationTransformer 接口

public class MyTransformer implements IAnnotationTransformer {
  public void transform(ITest annotation, Class testClass,
      Constructor testConstructor, Method testMethod)
  {
    if ("invoke".equals(testMethod.getName())) {
      annotation.setInvocationCount(5);////执行5次
    }
  }
}

2.运行cmd命令或者编程式运行。

TestNG tng = new TestNG()

【官方原话】

IAnnotationTransformer只允许您修改@Test注释。

如果需要修改另一个TestNG注释(@Factory或@DataProvider),请使用IAnnotationTransformer2接口

方法拦截器

一旦TestNG计算出调用测试方法的顺序,这些方法就被分成两组:

1.按顺序运行【包含依赖关系】

2.不按特定顺序运行

为了对属于第二类的方法有更多的控制,TestNG定义了以下接口:

public interface IMethodInterceptor {
  List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context);
}

入参:

​ 传入参数的方法列表是可以按任何顺序运行的所有方法。

返回:

​ 可以对入参方法列表进行编程,不改、缩减、扩大methods都可以

执行:

java -classpath "testng-jdk15.jar:test/build" org.testng.TestNG -listener test.methodinterceptors.NullMethodInterceptor
   -testclass test.methodinterceptors.FooTest

示例:

这里有一个方法拦截器,它将对方法重新排序,以便始终首先运行属于fast组的测试方法:

public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) {
  List<IMethodInstance> result = new ArrayList<IMethodInstance>();
  for (IMethodInstance m : methods) {
    Test test = m.getMethod().getConstructorOrMethod().getAnnotation(Test.class);
    Set<String> groups = new HashSet<String>();
    for (String group : test.groups()) {
      groups.add(group);
    }
    if (groups.contains("fast")) {
      result.add(0, m);
    }
    else {
      result.add(m);
    }
  }
  return result;
}

监听器

详细示例【 www.jianshu.com/p/2f9342406…

监听类型

​ 有几个接口允许您修改TestNG的行为。这些接口被广泛地称为“TestNG监听器”

IAnnotationTransformer (doc, javadoc)对注释进行转换,需要实现该接口,并重写transform 方法
IAnnotationTransformer2 (doc, javadoc)也是对注释进行转换,在上面的接口不满足的情况下,使用较少
IHookable (doc, javadoc) 执行测试方法前进行授权检查,根据授权结果执行测试
IInvokedMethodListener (doc, javadoc) 调用方法前、后启用该监听器,常用于日志的采集
IMethodInterceptor (doc, javadoc) 调用方法前、后启用该监听器,常用于日志的采集
IReporter (doc, javadoc) 运行所有套件时都将调用此方法,后续可用于自定义测试报告
ISuiteListener (doc, javadoc) 测试套件执行前或执行后嵌入相关逻辑
ITestListener (doc, javadoc)  常用TestListenerAdapter来替代

监听配置

​ 1.命令行

​ 2.ant命令

​ 3.xml配置

<suite>
  <listeners>
    <listener class-name="com.example.MyListener" />
    <listener class-name="com.example.MyMethodInterceptor" />
  </listeners>
</suite>

​ 或者

@Listeners({ com.example.MyListener.class, com.example.MyMethodInterceptor.class })
public class MyTest {
  // ...
}

4.使用ServiceLoader

自定义监听器

注意,@Listeners注释将应用于整个套件文件,就像您在testng.xml文件中指定它一样。

如果要限制其作用域(例如,仅在当前类上运行),侦听器中的代码可以首先检查即将运行的测试方法,然后决定

执行什么操作!

1.自定义一个新注解

@Retention(RetentionPolicy.RUNTIME)
@Target ({ElementType.TYPE})
public @interface DisableListener {}

2.监听检查

public void beforeInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
  ConstructorOrMethod consOrMethod =iInvokedMethod.getTestMethod().getConstructorOrMethod();
  DisableListener disable = consOrMethod.getMethod().getDeclaringClass().getAnnotation(DisableListener.class);
  if (disable != null) {
    return;
  }
  // 恢复正常操作
}

3.注释不调用监听器的测试类

@DisableListener
@Listeners({ com.example.MyListener.class, com.example.MyMethodInterceptor.class })
public class MyTest {
  // ...
}

依赖注入

注入类型

原生方法(由TestNG本身执行)

扩展方法(由依赖的注入框架执行,如:Guice)。

  • 任何@Before方法或@Test方法都可以声明类型为ITestContext的参数。
  • 任何@AfterMethod方法都可以声明类型为ITestResult的参数,该参数将反映刚刚运行的测试方法的结果。
  • 任何@Before和@After方法(@BeforeSuite和@AfterSuite除外)都可以声明一个XmlTest类型的参数,它包含当前的标记。
  • 任何@BeforeMethod(或@AfterMethod)可以声明java.lang.reflect.Method类参数。这个参数可以接受@BeforeMethod运行完之后调用的测试方法(或在方法运行@AfterMethod之后)。
  • 任何@BeforeMethod都可以声明类型为Object[]的参数。该参数包含注入下一个测试方法的参数列表,这些参数由TestNG注入,例如java.lang.reflect.Method或@DataProvider。
  • 任何@DataProvider都可以声明类型为ITestContext或java.lang.reflect.Method的参数。
注解 ITestContext XmlTest Method Object[] ITestResult
@BeforeSuite Yes No No No No
@BeforeTest Yes Yes No No No
@BeforeGroups Yes Yes No No No
@BeforeClass Yes Yes No No No
@BeforeMethod Yes Yes Yes Yes Yes
@Test Yes No No No No
@DataProvider Yes No Yes No No
@AfterMethod Yes Yes Yes Yes Yes
@AfterClass Yes Yes No No No
@AfterGroups Yes Yes No No No
@AfterTest Yes Yes No No No
@AfterSuite Yes No No No No
@NoInjection
package org.vk.test.springtest_testng;

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.vk.demo.EatCompentConfig;

import java.lang.reflect.Method;

public class Test2 {
    @DataProvider(name = "provider")
    public Object[][] provide() throws Exception {
        return new Object[][] { { EatCompentConfig.class.getMethod("hasNext") } };
    }

    @Test(dataProvider = "provider")
    public void withoutInjection(@NoInjection Method m) {
        Assert.assertEquals(m.getName(), "hasNext");
    }

    @Test(dataProvider = "provider")
    public void withInjection(Method m) {
        Assert.assertEquals(m.getName(), "withInjection");
    }
}

该注解是为了关闭依赖注入,为什么?

对于案例中,withoutInjection方法的入参和依赖注入中的默认对象有重叠,且默认情况下,使用的就是依赖

所对应的对象也就是说Method本身如果在不加@NoInjection的情况下,那么它是代表withoutInjection方法本身的,但是我们代码的意思,确是希望,传入一个入参Method而不是依赖注入默认的Method,所以我们需要该注解@NoInjection来关闭依赖注入,从而Assert断言成功!

8.测试报告

关于测试报告,技术选型很多种,我选用的是比较简单、好看的external-report插件,配合自定义的监听器实现

备注:target\surefire-reports\index.html,这里是最原始的测试报告,其他插件的报告位置可以自己定义

pom.xml

<!--testng依赖-->
<dependency>
	<groupId>org.testng</groupId>
	<artifactId>testng</artifactId>
	<version>7.1.0</version>
</dependency>
<!--测试报告的依赖-->
<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>

xml配置

<suite name="test2">
    <listeners>
      <listener class-name="org.vk.test.listeners.report.ExtentTestNGIReporterListener"/>
    </listeners>
    <test name="Test2" >
        <classes>
            <class name="org.vk.test.demos.Test2">
            </class>
        </classes>
    </test>
</suite>

自定义测试报告的监听器【照搬即可,没必要自己实现

package org.vk.test.listeners.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 com.aventstack.extentreports.reporter.configuration.Theme;
import org.testng.*;
import org.testng.xml.XmlSuite;

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

/**
 * TestNg生成好看的测试UI报告
 *
 * @author liuleiba@ecej.com
 * @version 1.0
 */
public class ExtentTestNGIReporterListener implements IReporter {
    //美化后的测试报告生成的路径以及文件名
    private static final String OUTPUT_FOLDER = "target/test-report/";
    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);
                }
            }

        }
        for (String s : Reporter.getOutput()) {
            extent.setTestRunnerOutput(s);
        }
        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);
        // 设置静态文件的DNS
        //怎么样解决cdn.rawgit.com访问不了的情况
        htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);

        htmlReporter.config().setDocumentTitle("PC端自动化测试报告");
        htmlReporter.config().setReportName("PC端自动化测试报告");
        htmlReporter.config().setChartVisibilityOnOpen(true);
        htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP);
        htmlReporter.config().setTheme(Theme.STANDARD);
        htmlReporter.config().setEncoding("gbk");
        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="";
                //如果有参数,则使用参数的toString组合代替报告中的name
                for(Object param:parameters){
                    name+=param.toString();
                }
                if(name.length()>0){
                    if(name.length()>50){
                        name= name.substring(0,49)+"...";
                    }
                }else{
                    name = result.getMethod().getMethodName();
                }
                if(extenttest==null){
                    test = extent.createTest(name);
                }else{
                    //作为子节点进行创建时,设置同父节点的标签一致,便于报告检索。
                    test = extenttest.createNode(name).assignCategory(categories);
                }
                //test.getModel().setDescription(description.toString());
                //test = extent.createTest(result.getMethod().getMethodName());
                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();
    }
}

结果输出页面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OkxMK1By-1581562336478)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\1579085189960.png)]

9.TestNg与Springboot结合

1. 编码规范

对于单元测试,我们希望您按照以下几种规则去设计、处理、编码:

1.命名规范

​ 测试包的根目录:必须在src/test/java下**[源码构建时会跳过此目录,单元测试框架默认是扫描此目录]**

​ 测试包中java类的包路径:与实际要测试的类,保持一致**[ 参考编写流程中的截图]**

​ 测试包的java类名:遵循OrderQueryService.java -> OrderQueryServiceTest.java规则

​ 测试包的xml路径:在实际要测试的类的包下,新建xml包即可,存放各个测试类型testng.xml

​ 测试包的监听器:在实际要测试的类的包下,根据监听器作用范围,新建listerners包即可,

2.设计原则

  • 全自动执行,而非交互式。mvn test或者运行对整个类的测试案例时,都可以自动运行完所有测试
  • 测试case要可靠,,并且是值得信赖的,对于相关底层的任何核心改动都要能够及时感知
  • 支持重复运行,并且保证覆盖率【if-else】能够基本覆盖所有场景。【普通语句70%,核心语句100%】
  • 必须要对运行结果做Assert断言,不允许使用System.out.print,使用日志log占位符输出测试信息
  • 纯数据库repository层的测试,不允许使用mock进行测试,并且保证要有数据回滚机制,不造成脏数据
  • 每一次项目版本的迭代、修改的同时,维护好测试案例,不允许对已经存在且运行完好的测试Ignore
  • 复杂接口,尽量拆解成单独的测试案例,保持较小的粒度有助于迅速发现并且精准的定位问题

2. 编写流程

1.目录结构与包配置

基础目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LbjdW9PA-1581562336480)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782404369.png)]

测试包配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8hS2NtoM-1581562336481)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782525627.png)]

2.pom引入依赖

<!--testng依赖-->
<dependency>
	<groupId>org.testng</groupId>
	<artifactId>testng</artifactId>
	<version>7.1.0</version>
</dependency>
<!--测试报告的依赖-->
<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>

3.编写测试类

@DataProvider注解入参
package com.ecej.order.basics.service.impl;

import com.alibaba.fastjson.JSON;
import com.ecej.order.basics.Startup;
import com.ecej.order.basics.api.query.OrderQueryService;
import com.ecej.order.basics.bean.dto.CombinedOrderDTO;
import com.ecej.order.basics.bean.dto.WorkOrderDetailDTO;
import com.ecej.order.basics.bean.request.WorkOrderDetailQueryReqParam;
import com.ecej.order.basics.bean.request.WorkOrderListQueryReqParam;
import com.ecej.order.common.util.DateUtil;
import com.ecej.order.model.baseResult.ResultMessage;
import com.ecej.order.listener.ExtentTestNGIReporterListener;
import com.ecej.order.util.QueryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.ITestContext;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.Date;

/**
 * @ClassName: &#x8ba2;&#x5355;&#x67e5;&#x8be2;&#x6d4b;&#x8bd5;
 * @Author: Administrator
 * @Description: zlr
 * @Date: 2020/1/16 17:54
 * @Version: 1.0
 */
@SpringBootTest(classes = Startup.class)
@Listeners(ExtentTestNGIReporterListener.class)
public class OrderQueryServiceImplTest extends AbstractTestNGSpringContextTests {
    private static final Logger logger = LoggerFactory.getLogger(OrderQueryServiceImplTest.class);

    @Autowired
    private OrderQueryService orderQueryService;

    @Test(dataProvider = "createOrderListQueryData", suiteName = "订单列表单元测试", groups = "queryWorkOrderList", timeOut = 10000)
    public void queryWorkOrderListPageTest(int paramType, ITestContext testContext, WorkOrderListQueryReqParam param) {
        logger.info("参数名称={};测试第[{}]次开始={}",paramType,testContext.getPassedTests().size()+1);
        ResultMessage<QueryResult<CombinedOrderDTO>> queryWorkOrderListPage = orderQueryService.queryWorkOrderListPage(param);
        logger.info(JSON.toJSONString(queryWorkOrderListPage));
        switch (paramType) {
            case 1:
                Assert.assertEquals(queryWorkOrderListPage.getCode(), 1000);
                break;
            case 2:
                Assert.assertEquals(queryWorkOrderListPage.getCode(), 1000);
                break;
            case 3:
                Assert.assertEquals(queryWorkOrderListPage.getCode(), 1000);
                break;
            case 4:
                Assert.assertEquals(queryWorkOrderListPage.getCode(), 200);
                break;
            default:
                Assert.assertEquals(queryWorkOrderListPage.getCode(), 200);
        }
    }

    @Test(dataProvider = "createOrderDetailData", suiteName = "订单详情单元测试", groups = "orderDetailAnnotations", timeOut = 10000)
    public void queryWorkOrderDetailTest(int paramType, WorkOrderDetailQueryReqParam param) {
        ResultMessage<WorkOrderDetailDTO> resultMessage = orderQueryService.queryWorkOrderDetail(param);
        switch (paramType) {
            case 1:
                Assert.assertEquals(resultMessage.getCode(), 1000);
                break;
            case 2:
                Assert.assertEquals(resultMessage.getCode(), 1000);
                break;
            case 3:
                Assert.assertEquals(resultMessage.getCode(), 200);
                break;
            default:
                Assert.assertEquals(resultMessage.getCode(), 200);
        }
        logger.info(JSON.toJSONString(resultMessage));
    }

    /**
     * 创建订单列表查询参数(此处也可根据查询数据库作为参数对象)
     * 构建多场景测试案例参数(单元测试根据场景 1,2,3,4 进行断言)
     */
    @DataProvider(name = "createOrderListQueryData")
    public Object[][] createOrderListQueryData() {
        //1、构建空对象
        WorkOrderListQueryReqParam checkParam = new WorkOrderListQueryReqParam();
        //2、构建残缺参数(requestSource)
        WorkOrderListQueryReqParam paramRequestSource = new WorkOrderListQueryReqParam();
        paramRequestSource.setCityId(2237);
        paramRequestSource.setCityIdList(Arrays.asList(2237, 2057, 2367));
        paramRequestSource.setBookStartTimeBegin(DateUtil.getDate(new Date(), -4));
        paramRequestSource.setWorkOrderStatusList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 150));
        paramRequestSource.setBookStartTimeEnd(DateUtil.getDate(new Date(), 4));
        paramRequestSource.setStationIdList(Collections.singletonList(35200342));
        paramRequestSource.setPageNum(1);
        paramRequestSource.setPageSize(10);
        paramRequestSource.setOrderDispatchingModeList(Arrays.asList(1, 2, 3, 4, 5));
        //3、构建订单来源是 99必填参数校验(缺少预约时间查询)
        WorkOrderListQueryReqParam paramBookStartTime = new WorkOrderListQueryReqParam();
        paramBookStartTime.setRequestSource(99);
        paramBookStartTime.setCityId(2237);
        paramBookStartTime.setCityIdList(Arrays.asList(2237, 2057, 2367));
        paramBookStartTime.setWorkOrderStatusList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 150));
        paramBookStartTime.setStationIdList(Collections.singletonList(35200342));
        paramBookStartTime.setPageNum(1);
        paramBookStartTime.setPageSize(10);
        paramBookStartTime.setOrderDispatchingModeList(Arrays.asList(1, 2, 3, 4, 5));
        //4、完整参数
        WorkOrderListQueryReqParam param = new WorkOrderListQueryReqParam();
        param.setRequestSource(99);
        param.setCityId(2237);
        param.setCityIdList(Arrays.asList(2237, 2057, 2367));
        param.setBookStartTimeBegin(DateUtil.getDate(new Date(), -4));
        param.setWorkOrderStatusList(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 150));
        param.setBookStartTimeEnd(DateUtil.getDate(new Date(), 4));
        param.setStationIdList(Collections.singletonList(35200342));
        param.setPageNum(1);
        param.setPageSize(10);
        param.setOrderDispatchingModeList(Arrays.asList(1, 2, 3, 4, 5));
        // param.setWorkOrderNo("4542");
        return new Object[][]{
                { 1, checkParam },
                { 2, paramRequestSource },
                { 3, paramBookStartTime },
                { 4, param },
        };
    }

    /**
     * 构建多个测试参数:创建订单接口
     */
    @DataProvider(name = "createOrderDetailData")
    public Object[][] createOrderDetailData() {
        //1、构建空对象
        WorkOrderDetailQueryReqParam checkParam = new WorkOrderDetailQueryReqParam();
        //2、构建残缺参数(WorkOrderNo)
        WorkOrderDetailQueryReqParam checkWorkOrderNoParam = new WorkOrderDetailQueryReqParam();
        checkWorkOrderNoParam.setRequestSource(99);
        //4、完整参数
        WorkOrderDetailQueryReqParam param = new WorkOrderDetailQueryReqParam();
        param.setRequestSource(99);
        param.setWorkOrderNo("A201801191022356151");
        return new Object[][]{
                { 1, checkParam },
                { 2, checkWorkOrderNoParam },
                { 3, param },
        };
    }


@Parameters方式入参
package com.ecej.order.basics.service.impl;

import com.alibaba.fastjson.JSON;
import com.ecej.order.basics.Startup;
import com.ecej.order.basics.api.query.OrderQueryService;
import com.ecej.order.basics.bean.dto.WorkOrderDetailDTO;
import com.ecej.order.basics.bean.request.WorkOrderDetailQueryReqParam;
import com.ecej.order.model.baseResult.ResultMessage;
import com.ecej.order.listener.ExtentTestNGIReporterListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.annotations.Listeners;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

/**
 * @ClassName: OrderTGXml
 * @Author: Administrator
 * @Description: zlr
 * @Date: 2020/1/16 13:51
 * @Version: 1.0
 */
@SpringBootTest(classes = { Startup.class })
@Listeners(ExtentTestNGIReporterListener.class)
public class OrderQueryServiceImplXmlTest extends AbstractTestNGSpringContextTests {
    private static final Logger logger = LoggerFactory.getLogger(OrderQueryServiceImplXmlTest.class);
    @Autowired
    private OrderQueryService orderQueryService;

    @Test(groups = "queryWorkOrderDetail")
    @Parameters({"requestSource","workOrderNo"})
    public void queryWorkOrderDetailTest(Integer requestSource,String workOrderNo){
        WorkOrderDetailQueryReqParam param = new WorkOrderDetailQueryReqParam();
        param.setRequestSource(requestSource);
        param.setWorkOrderNo(workOrderNo);
        ResultMessage<WorkOrderDetailDTO> resultMessage = orderQueryService.queryWorkOrderDetail(param);
        Assert.assertEquals(resultMessage.getCode(), 200);
        logger.info(JSON.toJSONString(resultMessage));
    }
}

基本上,和平常写代码区别不大,只需额外维护TG的xml和它自己的一些注解【架构的案例已经满足】

4.编排测试类xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="订单基础服务单元测试报告" parallel="classes" thread-count="1">
    <listeners><listener class-name="com.ecej.order.listener.ExtentTestNGIReporterListener"/></listeners>
    <test verbose="1" preserve-order="true" name="订单查询">
        <parameter name="requestSource" value="99" />
        <parameter name="workOrderNo" value="A201801191022356151"/>

        <groups>
            <define name="queryWorkOrderListPageTest">
                <!--可以是多个,也可以分开写-->
                <include name="queryWorkOrderList"/>
                <!--<include name="queryWorkOrderDetail"/>-->
            </define>
            <define name="queryWorkOrderDetailTest">
                <include name="queryWorkOrderDetail"/>
            </define>
            <run>
                <include name="queryWorkOrderListPageTest"/>
                <include name="queryWorkOrderDetailTest"/>
            </run>
        </groups>

        <classes>
            <!-- 测试类可以多个 -->
            <class name="com.ecej.order.basics.service.impl.OrderQueryServiceImplTest" />
            <class name="com.ecej.order.basics.service.impl.OrderQueryServiceImplXmlTest" />
        </classes>
    </test>
</suite>

xml是在测试类的基础上二次编排,最终的测试效果是以xml为准

3. 运行测试案例

1. IDEA运行单个测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2hhy4e4-1581562336483)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782613491.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fZjOfE51-1581562336483)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782642269.png)]

2. IDEA运行整个包的测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5THIiuIq-1581562336485)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782705068.png)]

3. Eclipse运行单个测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k6C9V2B2-1581562336485)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782761162.png)]

4. Eclipse运行整个包的测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5QUh17b-1581562336485)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782774727.png)]

5. Debug断点查看

Debug和正常程序一样,可以使用debug模式启动测试案例,对其中的某些入参、返回做断点,查看参数、返回

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cR7dgk8C-1581562336486)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580782877064.png)]

6. MVN运行整个项目测试

本案例中执行mvn test的运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQjPjJpq-1581562336487)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580783218995.png)]

如果出现结果错误时,需要对Failed tests中的错误测试,进行修复,直至Build Success为止

4. 查看测试案例报告

某些情况下,控制台可以看到测试结果,但是对于项目发布,我们最好还是从测试结果报告中查看统计信息

我们可以从两种类型的报告中获取测试案例的运行情况,哪些成功、失败,从而对它们进行修复。

具体查看哪一种报告,看个人习惯。

report-ng原始报告

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zzZpRQHQ-1581562336487)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580783311156.png)]

external-report美化报告

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQJqiPaa-1581562336487)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580783343860.png)]

5.测试案例的数据处理【1】

场景分析

对于某些方法,对数据完整性依赖性较高,且手动构建数据复杂,测试场景需要全面的时候,需要通过测试案例来模拟完整的:入库-查询-修改-删除场景时,那么我们在运行某些query的测试案例前,需要插入一些数据来支撑其他测试运行,在运行完测试之后,我们又需要擦除利用完了的数据

至此,我们需要有相应的手段和case来覆盖这些场景

那么在编写测试案例中,对于service、dao层的curd操作,所产生的脏数据问题,我们需要从两个角度去考虑:

1.本地服务数据擦除

对于本项目中service、dao数据的回滚,可以从以下3个方面考虑:

1.自动回滚【推荐使用】

有两个类需要说明一下:

1.AbstractTestNGSpringContextTests

​ 测试类只有继承了该类才能拥有注入实例Bean的能力,否则注入报错

​ 总结:【适合处理查询的测试案例】

2.AbstractTransactionalTestNGSpringContextTests

​ 测试类继承该类后拥有注入实例能力,同时拥有事物控制能力

​ 总结:【适应于任何场景,推荐使用】

所以,处理本地项目中的servicedao,对于数据库产生的数据,我们只需要将测试类继承AbstractTransactionalTestNGSpringContextTests即可,测试案例中所有对数据库的操作将都只停留在测试阶段,一旦测试案例运行完成,TestNG会自动帮助我们回滚数据,没有任何的代码侵入。

示例:

/**
 * 演示Test Curd操作【远程服务事物回滚:编程式回滚】
 *
 * @author liulei, liuleiba@ecej.com
 * @version 1.0
 */
@SpringBootTest(classes = Startup.class)
@Listeners(ExtentTestNGIReporterListener.class)
    public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests {

    private static final Logger logger = LoggerFactory.getLogger(TestNgCurdServiceImplTest.class);

    @Autowired
    TestNgCurdService testNgCurdService;

    @DataProvider
    private Object[][] saveOrUpdateParam() {
        SysMenuParam po = new SysMenuParam();
        po.setLevels(1);
        po.setMenuSort(1);
        po.setMenuName("测试菜单");
        po.setMenuUrl("menu—url");
        po.setPmenuId("1");
        return new Object[][]{{po}};
    }

    /**
     * 1.测试保存
     */
    @Rollback
    @Test(groups = "saveOrUpdate", dataProvider = "saveOrUpdateParam")
    public void testSaveOrUpdate(SysMenuParam po) {
        logger.info("测试保存开始:{}", po);
        ResultMessage<Boolean> result = testNgCurdService.saveOrUpdate(po);
        Assert.assertEquals(result.getCode(), 200);
        logger.info("测试保存结束", result);
    }
    //...   
}        
2.选择性回滚

测试类继承AbstractTestNGSpringContextTests类,搭配使用@Rollback注解,对部分测试方法进行事物回滚,避免测试案例过程中的测试数据最后成为了脏数据,运行完测试案例后,TestNG会自动帮助我们回滚对应注解了@Rollback方法所产生的数据,其他没有加注解的方法则会真实作用于数据库层面,慎用

示例:

public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests {
//....
//写法保持不变,不需要改动任何代码,只需要在需要回滚的方法上加上注解@Rollback注解即可
//....
}
3.编程式回滚【慎用】

手动编程,将测试案例运行前后,产生的所有数据,手动调用相关的删除delete接口逐一擦除【适合远程服务】

下面章节【远程服务数据回滚】有相关的案例和说明!

2.远程服务数据擦除

如果一个测试案例以非Mock方式运行,并且有对远程服务进行调用,产生了脏数据,那么此时只能通过编程式回滚数据,即在执行完测试案例的前后,调用相关delete方法,进行清除,如果测试案例上下文较为复杂,对数据的回收分析就变得比较重要,并且服务间链式调用过长,一旦测试案例产生了错误,那么会产生不可预知的一些问题,需要谨慎使用。

本例中,我们也有相关的case覆盖:

核心流程:

​ 测试远程服务的增删改查,则必然需要在查询方法前,我们需要插入准备数据,才能测试查询接口,我们可以通过TestNg提供的执行机制,在运行完测试案例之后调用相关的delete方法,清除运行期间临时准备的测试数据,否则会污染数据库。

有兴趣的小伙伴可以根据以下代码自行测试一下

1.service接口及其实现类
/**
 * TestNg Curd测试案例【无实际用途】
 * @author liulei, lei.liu@htouhui.com
 * @version 1.0
 */
public interface TestNgCurdService {

    ResultMessage<Boolean> saveOrUpdate(SysMenuParam sysMenuParam);

    ResultMessage<Boolean> delete(SysMenuParam sysMenuParam);

    ResultMessage<List<SysMenuDTO>> queryList(SysMenuReqParam sysMenuReqParam);

}
2.测试类
package com.ecej.order.basics.service.impl;

import com.ecej.order.basics.Startup;
import com.ecej.order.basics.api.query.TestNgCurdService;
import com.ecej.order.listener.ExtentTestNGIReporterListener;
import com.ecej.order.model.baseResult.ResultMessage;
import com.ecej.order.strategy.bean.dto.SysMenuDTO;
import com.ecej.order.strategy.bean.request.SysMenuParam;
import com.ecej.order.strategy.bean.request.SysMenuReqParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.annotations.*;

import java.util.List;

/**
 * 演示Test Curd操作【远程服务事物回滚:编程式回滚】
 *
 * @author liulei, liuleiba@ecej.com
 * @version 1.0
 */
@SpringBootTest(classes = Startup.class)
@Listeners(ExtentTestNGIReporterListener.class)
public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests {

    private static final Logger logger = LoggerFactory.getLogger(TestNgCurdServiceImplTest.class);

    @Autowired
    TestNgCurdService testNgCurdService;

    @DataProvider
    private Object[][] saveOrUpdateParam() {
        SysMenuParam po = new SysMenuParam();
        po.setLevels(1);
        po.setMenuSort(1);
        po.setMenuName("测试菜单");
        po.setMenuUrl("menu—url");
        po.setPmenuId("1");
        return new Object[][]{{po}};
    }

    /**
     * 1.测试保存
     */
    @Test(groups = "saveOrUpdate", dataProvider = "saveOrUpdateParam")
    public void testSaveOrUpdate(SysMenuParam po) {
        logger.info("测试保存开始:{}", po);
        ResultMessage<Boolean> result = testNgCurdService.saveOrUpdate(po);
        Assert.assertEquals(result.getCode(), 200);
        logger.info("测试保存结束:{}", result);
    }

    @DataProvider
    private Object[][] queryListParam() {
        SysMenuReqParam po = new SysMenuReqParam();
        po.setMenuName("测试菜单");
        return new Object[][]{{po}};
    }

    /**
     * 2.测试查询【xml文件中别忘记使用allow-return-values="true" 注解来强制返回测试案例结果,以便手动清除数据】
     */
    @Test(groups = "queryList", dataProvider = "queryListParam", dependsOnGroups = "saveOrUpdate")
    public List<SysMenuDTO> testQueryList(SysMenuReqParam po) {
        logger.info("测试查询开始:{}", po);
        ResultMessage<List<SysMenuDTO>> result = testNgCurdService.queryList(po);
        Assert.assertEquals(result.getCode(), 200);
        logger.info("测试查询结束:{}", result);
        return result.getData();
    }

    /**
     * 3.测试根据menuId删除方法【依赖于插入方法】
     */
    public void testDelete(SysMenuParam po) {
        logger.info("测试删除开始:{}", po);
        ResultMessage<Boolean> result = testNgCurdService.delete(po);
        //预期删除成功,但目前的远程接口,插入、查询后返回菜单主键,所以此处会失败【注意】
        Assert.assertEquals(result.getCode(), 303);
        logger.info("测试删除结束:{}", result);
    }

    /**
     * 由于是远程服务,无法通过test-ng的回滚机制来回顾测试数据
     * 所以,对于远程服务测试案例的curd,必须要通过手动清除测试数据,来保证数据的纯洁度
     */
    @AfterSuite
    public void clearData() {
        //此处由于插入方法没有返回主键,我们需要将新插入的主键查询出来后进行删除
        SysMenuReqParam po = new SysMenuReqParam();
        po.setMenuName("测试菜单");
        SysMenuParam sysMenuParam = new SysMenuParam();
        List<SysMenuDTO> sysMenuDTO = testQueryList(po);
        for (SysMenuDTO dto : sysMenuDTO) {
            SysMenuParam param = new SysMenuParam();
            //此处由于远程接口没有返回主键ID,所以运行删除此处会报错,但是目前也无法更改远程接口,所以大家注意即可
            BeanUtils.copyProperties(dto, param);
            testDelete(sysMenuParam);
        }
    }
}
3.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="TestNg测试Curd套件" allow-return-values="true" parallel="classes" thread-count="1">
    <listeners><listener class-name="com.ecej.order.listener.ExtentTestNGIReporterListener"/></listeners>
    <test verbose="1" preserve-order="true" name="订单查询">
        <parameter name="requestSource" value="99" />
        <parameter name="workOrderNo" value="A201801191022356151"/>

        <groups>
            <define name="saveOrUpdate"/>
            <define name="queryList"/>
            <dependencies>
                <group name="queryList" depends-on="saveOrUpdate"/>
            </dependencies>
        </groups>

        <classes>
            <!-- 测试类可以多个 -->
            <class name="com.ecej.order.basics.service.impl.TestNgCurdServiceImplTest" />
        </classes>
    </test>
</suite>

至此,事物的回滚已经完成,但是也存在一些问题,就是这一整个链路任何一个环节要是出问题,都会产生脏数据,比如新增完之后,delete方法报错,那么插入数据库中的数据就会保留,需要手动去清除库,调用链路如果比较长,涉及面较广的时候,存在不确定性!

6.测试案例的数据处理【2】

章节【1】中是处理测试案例数据的一种方式,是以测试方法为维度进行处理

另外一种方式,则是利用TestNG的监听器,来做数据埋点,预先在数据库中初始化将要使用到的数据,我们以套件为单位来做埋点,范围过大则不推荐,一旦部分程序出现问题,容易导致数据混乱,不好处理。

该场景,我们仍然有相关的case覆盖:

核心流程

​ 执行测试套件suit之前,进行数据埋点,整个套件测试案例执行完之后,进行数据销毁

​ 以套件为单位,以sql脚本为介质进行处理

有兴趣的小伙伴可以根据以下代码自行测试一下:

测试套件监听器
package com.ecej.order.listener;

import com.ecej.order.util.TestDataHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.ISuite;
import org.testng.ISuiteListener;

/**
 * 测试数据埋点处理【需要埋点的测试类,直接使用此监听器即可】
 *
 * @author liulei, liuleiba@ecej.com
 * @version 1.0
 */
public class TestCaseDataPrepareListener implements ISuiteListener {

    private static final Logger logger = LoggerFactory.getLogger(TestCaseDataPrepareListener.class);

    /**
     * 埋点数据sql初始化脚本
     */
    private static String INIT_FILE = "order_init.sql";
    /**
     * 埋点数据sql销毁脚本
     */
    private static String DESTROY_FILE = "order_destroy.sql";

    private static TestDataHandler TestDataHandler = new TestDataHandler();

    /**
     * 测试套件执行前
     *
     * @param suite 套件
     */
    @Override
    public void onStart(ISuite suite) {
        logger.info("测试套件开始初始化测试数据");
        TestDataHandler.testDataOperate(INIT_FILE, false);
        logger.info("测试套件完成初始化测试数据");
    }

    /**
     * 测试套件执行后
     *
     * @param suite 套件
     */
    @Override
    public void onFinish(ISuite suite) {
        logger.info("测试套件开始初始化销毁数据");
        TestDataHandler.testDataOperate(DESTROY_FILE, true);
        logger.info("测试套件完成初始化销毁数据");
    }

}

埋点数据处理器
package com.ecej.order.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.io.*;
import java.sql.*;
import java.util.ArrayList;

/**
 * TODO 优化存储成ThreadLocalMap,初始化一次数据源即可后续复用数据源即可
 *
 * @author liulei, lei.liu@htouhui.com
 * @version 1.0
 */
public class TestDataHandler {

    private static final Logger logger = LoggerFactory.getLogger(TestDataHandler.class);

    private static String DB_DRIVER = "com.mysql.jdbc.Driver";
    private static String DB_URL = "jdbc:mysql://10.4.98.14:3306/ecejservice?useunicode=true&amp;characterencoding=utf-8&amp;zeroDateTimeBehavior=convertToNull";
    private static String DB_USER = "dev_user";
    private static String DB_PWD = "123qweasd";
    /**
     * 运行环境
     * TODO 待改成动态
     */
    private static String PROFILE = "dev";

    private static Connection connection;

    static {
        try {
            //加载mysql的驱动类
            Class.forName(DB_DRIVER);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 构造函数,包括连接数据库等操作
     */
    public TestDataHandler() {
        try {
            //加载mysql的驱动类
            Class.forName(DB_DRIVER);
            //获取数据库连接
            connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PWD);
        } catch (Exception e) {
            e.printStackTrace();
            connection = null;
        }
    }

    /**
     * 自定义数据库连接
     *
     * @param dbUrl    数据库连接
     * @param User     用户
     * @param Password 密码
     */
    public TestDataHandler(String dbUrl, String User, String Password) {
        try {
            //获取数据库连接
            connection = DriverManager.getConnection(dbUrl, User, Password);
        } catch (Exception e) {
            e.printStackTrace();
            connection = null;
        }
    }

    /**
     * 获取连接
     *
     * @return 连接conn
     */
    public Connection getConnection() {
        return connection;
    }

    /**
     * 释放数据库连接
     */
    public static void ReleaseConnect() {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 批量执行SQL语句
     *
     * @param sql     包含待执行的SQL语句的ArrayList集合
     * @param ifClose 是否关闭数据库连接
     * @return int 影响的函数
     */
    public int executeSqlFile(ArrayList<String> sql, boolean ifClose) {
        try {
            Statement st = connection.createStatement();
            for (String subsql : sql) {
                st.addBatch(subsql);
            }
            st.executeBatch();
            return 1;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        } finally {
            if (ifClose) {
                ReleaseConnect();
            }
        }
    }

    /**
     * 以行为单位读取文件,并将文件的每一行格式化到ArrayList中,常用于读面向行的格式化文件
     *
     * @param filePath 文件路径
     */
    private static ArrayList<String> readFileByLines(String filePath) throws Exception {
        ArrayList<String> listStr = new ArrayList<>();
        StringBuffer sb = new StringBuffer();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"));
            String tempString;
            int flag = 0;
            // 一次读入一行,直到读入null为文件结束
            while ((tempString = reader.readLine()) != null) {
                // 显示行号,过滤空行
                if (tempString.trim().equals(""))
                    continue;
                if (tempString.substring(tempString.length() - 1).equals(";")) {
                    if (flag == 1) {
                        sb.append(tempString);
                        listStr.add(sb.toString());
                        sb.delete(0, sb.length());
                        flag = 0;
                    } else
                        listStr.add(tempString);
                } else {
                    flag = 1;
                    sb.append(tempString);
                }
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e1) {
                }
            }
        }

        return listStr;
    }

    /**
     * 读取文件内容到SQL中执行
     *
     * @param file    SQL文件的路径
     * @param ifClose 是否关闭数据库连接
     */
    public void testDataOperate(String file, boolean ifClose) {
        try {
            Resource resource = new ClassPathResource("sql" + File.separator + PROFILE + File.separator + file);
            ArrayList<String> sqlStr = readFileByLines(resource.getFile().getAbsolutePath());
            if (sqlStr.size() > 0) {
                int num = executeSqlFile(sqlStr, ifClose);
                if (num > 0)
                    logger.info("sql[{}]执行成功", sqlStr);
                else
                    logger.error("有未执行的SQL语句", sqlStr);
            } else {
                logger.info("sql执行结束");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
埋点sql批处理脚本

我们根据不同的运行环境,设置不同的sql脚本,以免数据混乱,核心属性Profile【开发、测试环境】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Goszc2CT-1581562336489)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580965050323.png)]

测试类
package com.ecej.order.basics.service.impl;

import com.alibaba.fastjson.JSON;
import com.ecej.order.basics.Startup;
import com.ecej.order.basics.api.query.OrderQueryService;
import com.ecej.order.basics.bean.dto.WorkOrderDetailDTO;
import com.ecej.order.basics.bean.request.WorkOrderDetailQueryReqParam;
import com.ecej.order.listener.ExtentTestNGIReporterListener;
import com.ecej.order.listener.TestCaseDataPrepareListener;
import com.ecej.order.model.baseResult.ResultMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.annotations.Listeners;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

/**
 * 订单埋点测试,预先插入数据,运行完测试用例后自动删除埋点数据
 * <p>
 * 埋点监听器TestCaseDataPrepareListener
 *
 * @author liulei, liuleiba@ecej.com
 * @version 1.0
 */
@SpringBootTest(classes = Startup.class)
@Listeners({ExtentTestNGIReporterListener.class, TestCaseDataPrepareListener.class})
public class TestNgDataPrepareTest extends AbstractTestNGSpringContextTests {

    private static final Logger logger = LoggerFactory.getLogger(OrderQueryServiceImplXmlTest.class);

    @Autowired
    private OrderQueryService orderQueryService;

    @Parameters({"requestSource", "workOrderNo"})
    @Test(groups = "queryWorkOrderDetailTest")
    public void queryWorkOrderDetailTest(@Optional("99") Integer requestSource,
                                         @Optional("A201801191022356151") String workOrderNo) {
        WorkOrderDetailQueryReqParam param = new WorkOrderDetailQueryReqParam();
        param.setRequestSource(requestSource);
        param.setWorkOrderNo(workOrderNo);
        ResultMessage<WorkOrderDetailDTO> resultMessage = orderQueryService.queryWorkOrderDetail(param);
        Assert.assertEquals(resultMessage.getCode(), 200);
        //断言埋点数据的值和sql匹配
        Assert.assertEquals(resultMessage.getData().getOrderServiceInfo().getWorkOrderId().intValue(), 2000000);
        Assert.assertEquals(resultMessage.getData().getOrderServiceInfo().getWorkOrderNo(), "A201801191022356151");
        logger.info("订单详细工作信息:{}", JSON.toJSONString(resultMessage));
    }

}
xml测试
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="订单基础服务单元测试报告2" parallel="classes" thread-count="1">
    <listeners>
        <listener class-name="com.ecej.order.listener.ExtentTestNGIReporterListener"/>
        <listener class-name="com.ecej.order.listener.TestCaseDataPrepareListener"/>
    </listeners>
    <test verbose="1" preserve-order="true" name="订单查询">
        <groups>
            <run>
                <include name="queryWorkOrderDetailTest"/>
            </run>
        </groups>

        <classes>
            <!-- 测试类可以多个 -->
            <class name="com.ecej.order.basics.service.impl.TestNgDataPrepareTest" />
        </classes>
    </test>
</suite>
测试案例运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dNQfpEQI-1581562336489)(C:\Users\86151\AppData\Roaming\Typora\typora-user-images\1580965229125.png)]

7.测试案例数据问题的总结

1.对于本地服务,很显然【自动回滚】的方式最佳,无代码侵入,安全、干净,实现简便。

2.对于远程服务,显然是Mock的方式处理测试案例效果更好,mock测试本身就是一种假定,不会对数据库的数据产生实际影响,设计者只需要关心测试案例的核心业务,而不需要操心因环境、数据所带来的额外问题。

10.Mockito与TestNg结合案例

Why Mock

  1. 传统的测试案例,是以启动整个Spring容器为代价进行测试,否则对于某些深度DI的bean无法进行测试,耗时较长。而使用mockito可以对不必要的bean进行过滤,启动耗时短。

  2. 对于测试案例的内部bean的实现细节,mockito可以做细节控制,细化到测试方法内部的一些调用参数、执行次数、返回值的控制。更偏向于测试内部细节的把控以及业务编排。传统的测试案例无法做到。

  3. Mockito最强大的一点,是可以对测试类的依赖bean进行mock。

    在某些情况下,对于某些深度依赖的bean或者是远程服务bean,而且这些bean或者服务基本可以确保没有问题,只是本地环境有限,无法产生实际的调用时,就可以使用mockito对这些bean进行mock,这样不会产生实际的调用,但是又能够在测试案例中完整的模拟出调用的功能时。使用传统的测试案例任何一点出差错,都会导致运行结果失败。

  4. 和TestNg无缝结合,即可以使用mock来做预言,也可以使用TestNg的功能特性。

Mock测试流程

mockito与章节8中的testng测试案例无任何区别,只是在编写java测试类这一环节有区别

1.注解Mockito监听器MockitoTestExecutionListener

2.引入测试类的实现bean,以及它所直接依赖的bean

3.将直接依赖的bean进行Mock,即@MockBean

4.编写测试案例,对测试方法内部的实现进行mock级别的预言和对结果的断言

package com.ecej.order.basics.service.impl;

import com.alibaba.fastjson.JSON;
import com.ecej.model.po.SvcOrderDailyStatisticsPo;
import com.ecej.order.base.dao.order.OrderDailyStatisticsDao;
import com.ecej.order.basics.api.query.OrderQueryService;
import com.ecej.order.basics.bean.dto.SvcOrderDailyStatisticsDTO;
import com.ecej.order.basics.bean.request.OrderDailyStatisticsReqParam;
import com.ecej.order.basics.manager.OrderQueryManager;
import com.ecej.order.common.enums.MessageEnum;
import com.ecej.order.common.util.DateUtil;
import com.ecej.order.model.baseResult.ResultMessage;
import com.ecej.order.test.listener.ExtentTestNGIReporterListener;
import com.ecej.order.test.testng.OrderStatisticsMockitoTest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.annotations.*;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import static org.mockito.Mockito.*;

/**
 * 订单查询:TestNg + Mock简单测试
 *
 * @author liuleiba@ecej.com
 * @date 2020年1月16日 下午5:37:09
 */
@TestExecutionListeners(listeners = MockitoTestExecutionListener.class)
@Listeners(ExtentTestNGIReporterListener.class)
@ContextConfiguration(classes = {OrderQueryServiceImpl.class, OrderDailyStatisticsDao.class, OrderQueryManager.class})
public class OrderQueryServiceImplMockitoTest extends AbstractTestNGSpringContextTests {

    private static final Logger logger = LoggerFactory.getLogger(OrderStatisticsMockitoTest.class);

    @Autowired
    private OrderQueryService orderQueryService;

    @MockBean
    private OrderDailyStatisticsDao orderDailyStatisticsDao;

    @MockBean
    private OrderQueryManager orderQueryManager;

    /**
     * 构建多个测试参数,尽可能覆盖所有可能出现的场景
     */
    @DataProvider
    private Object[][] mockParam() {
        //1.构建空对象
        OrderDailyStatisticsReqParam param1 = new OrderDailyStatisticsReqParam();
        //2.构建残缺参数1
        OrderDailyStatisticsReqParam param2 = new OrderDailyStatisticsReqParam();
        param2.setQueryTime(DateUtil.getDate(new Date(), -2));
        //3.构建残缺参数2
        OrderDailyStatisticsReqParam param3 = new OrderDailyStatisticsReqParam();
        param3.setStationId(35200372);
        //4.构建完整参数
        OrderDailyStatisticsReqParam param4 = new OrderDailyStatisticsReqParam();
        param4.setQueryTime(DateUtil.getDate(new Date(), -2));
        param4.setStationId(35200372);
        return new Object[][]{{1, param1}, {2, param2}, {3, param3}, {4, param4}};
    }

    /**
     * 订单查询测试案例
     *
     * @param index 参数索引值
     * @param param 实际测试的入参
     */
    @Test(groups = "orderSearchManager", dataProvider = "mockParam", alwaysRun = true)
    public void orderSearchManageServiceMockTest(int index, OrderDailyStatisticsReqParam param) {
        logger.info("测试第[{}]次开始:订单日报统计查询入参:{}", index, JSON.toJSONString(param));

        //1.实际调用对应test的方法
        ResultMessage<SvcOrderDailyStatisticsDTO> result = orderQueryService.queryOrderDailyStatistics(param);
        logger.info("测试第[{}]次结束:订单日报统计查询结果:{}", index, result.getMessage());
        if (index < 4) {
            //对多个测试实例的错误测试的结果,进行断言
            Assert.assertEquals(result.getCode(), 1000);
            return;
        }

        //2.对测试方法运行过程中,对可能存在的dao调用进行mock模拟,并预言返回值
        ResultMessage<List<SvcOrderDailyStatisticsPo>> daoResult =
                new ResultMessage(MessageEnum.SUCCESS.getValue(), MessageEnum.SUCCESS.getDesc(), Arrays.asList());
        when(orderDailyStatisticsDao.queryOrderDailyStatistics(any())).thenReturn(daoResult);

        //3.对测试service方法产生的dao调用次数进行断言
        verify(orderDailyStatisticsDao, times(1)).queryOrderDailyStatistics(any());

        //4.对返回结果做断言
        Assert.assertEquals(result.getCode(), 200);
    }
}

11.参考资料

【官网】 testng.org/doc/documen…

【教程】 www.bbsmax.com/A/kvJ3Ypq7d…

【实战】 www.jianshu.com/p/880f5eeba…


今天就说这么多,如果本文对您有一点帮助,希望能得到您一个点赞👍哦

您的认可才是我写作的动力!


整理了一些Java方面的架构、面试资料(微服务、集群、分布式、中间件等),有需要的小伙伴可以关注公众号【程序员内点事】,无套路自行领取