用单元测试Junit彻底能够知足平常开发自测,为何还要学习TestNG,都影响了个人开发进度!html
最近技术部老大忽然宣布:全体开发人员必须熟练掌握自动化测试框架TestNG,就有了上边同事们的抱怨,是的,开始我也在抱怨,由于并不知道它是个什么东东,但从开始接触到慢慢编写测试用例,应用到项目后,我发现它真的超实用。java
咱们来一块儿看看它比Junit好在哪?node
TestNG[后面都简称为TG]是一款为了大量测试(好比测试时多接口数据依赖)须要,所诞生的一款测试框架,从简单的单元测试再到集成测试甚至是框架级别的测试,均可以覆盖到,所以是一款很是强大的测试框架!mysql
常规的TG的测试案例有三个步骤linux
运行整个项目的test-ng测试案例【若是是多模块项目,则是进入到对应的模块目录运行命令或者配置】git
xmlgithub
<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命令web
此处的命令会优先于pom.xml
中的配置,相对于java命令更优,由于mvn处理打包的一环,方便监控正则表达式
mvn clean package/install -DskipTests mvn clean package/install -Dmaven.test.skip=true #或者直接执行 mvn test
注解 | 做用 |
---|---|
@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方法都为测试方法 |
备注:相关对应的属性配置值,点进去对应类中查询便可,不在一一赘述spring
由xml文件表示,包含一个或者多个测试案例,使用<suite>标签包裹
通常来说,一个xml的<suite>
对应一个java类,除非特殊状况,在java中须要特别指定<suite>
不然xml对应java类的全部@Test注解属性suiteName默认都是xml中定义的<suite name='xxx'>
由<test>标签表示,包含一个或者多个TestNG的类
这些测试类须要在提交新代码前运行,保证基本功能不会被破坏
这些测试应该覆盖软件的全部功能,而且天天至少运行一次,即便有些状况下你不想运行它
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中定义的一、二、3方法都会被运行,若是换成check-intest,则只运行方法一、2
总结:
1.xml文件定义测试案例的运行策略
2.所谓的测试案例类别,只是概念上的定义
--- 登记类的测试案例,若是不在xml编排中没有涉及,它不必定运行
--- 功能性测试类,是咱们在xml编排中,必定会运行的测试案例
@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的命名所引诱】
【官方原话,不建议使用此类写法】
若是您开始重构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
java类,包含至少一个TG的注解,由<class>表示
java方法,含有@Test注解,默认状况下,test方法的返回值都会忽略,除非声明须要返回
<suite allow-return-values="true"> <!--或者--> <test allow-return-values="true">
测试方法组,不只能够定义方法属于哪一个group,还能够设置group包含哪些子group,TG会自动调用
能够在testng.xml的<test>or<suite>
中定义
若是在<suite>中指定组“a”,在<test>中指定组“b”,则“a”和“b”都将包括在内
@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”属性单独禁用测试。
@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组
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的每一个section部分,均可以在ant、命令行对应的文档中找到【另外2种调用TG的方式】
我的理解:通常来说,直接在idea、eclipse中运行@Test类也能够,但为何咱们须要testng.xml?
缘由:若是须要对java类、方法的测试案例进行编排,不使用xml进行编排,仅仅提供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>
- XML参数映射到Java参数的顺序与注释中的顺序相同,若是数量不匹配,TestNG将发出一个错误。
- 参数的做用域:
在testng.xml中,能够在<suite>标记下或<test>下声明它们。
若是两个参数具备相同的名称,则在<test>中定义的参数具备优先权。若是您须要指定一个适用于全部测试的参数,并仅对某些测试重写其值,则这很方便。
@Parameters("hello") @Test(groups = {"functest"}) public void testMethod4(@Optional("hello") String hello) { System.out.println(hello); }
【官方说明】
@Parameters注解,一样适用于@Before/After
and@Factory
此类的注解!结果:输出hello字符串
总结:以上的方式,适合简单参数配置,不适合作复杂对象注入
这种方式是为了弥补第一种方式而衍生的,若是参数构建比较复杂,复杂对象没法在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构造实例的方法必须返回Object[]。
下一个小节的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中,颜色既不是红色也不是绿色),这一点很重要,由于跳过的方法不必定是失败的。
高级用法参照【 http://beust.com/weblog/2004/... 】
即便有些方法失败了,你也会一直在追求你所依赖的方法。
当您只想确保您的测试方法以特定的顺序运行,但它们的成功并不真正依赖于其余方法的成功时,这很是有用。
经过在@Test注释中添加alwaysRun=true
得到软依赖性。
测试案例按照实例进行分组运行
一般的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="2" 标识的就是记录的日志级别,共有0-10的级别,其中0表示无,10表示最详细
默认就是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>
若是您运行多个套件文件(例如“java org.testng.testng testng1.xml testng2.xml”),而且但愿这些套件在单独的线程中运行,那么这很是有用。可使用如下命令行标志指定线程池的大小:
java org.testng.TestNG -suitethreadpoolsize 3 testng1.xml testng2.xml testng3.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将在同一线程中运行同一实例中的全部方法,但两个不一样实例上的两个方法将在不一样线程中运行。-->
从三个不一样的线程调用函数testServer十次。10秒的超时保证没有一个线程会永远阻塞这个线程。
@Test(threadPoolSize = 3, invocationCount = 10, timeOut = 10000)//timeOut无论是否多线程都有效 public void testServer() { ... ... }
每次在套件中测试失败时,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:UsersdellAppDataRoamingTyporatypora-user-images1579156460298.png)]
若是测试案例出现错误,想要启用TG的重试步骤以下:
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(); } }
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 | 逗号分割的测试名称 | 只有测试案例的 <test> 匹配上此处的配置才会运行 | ||
-testrunfactory | 逗号分隔的classpath下能够找到的java类 | 容许本身定义要运行的类. 类必需要实现org.testng.ITestRunnerFactory |
||
-threadcount | 数字 | 设置并发运行测试案例的最大线程数.只有使用-parallel选项才会生效 若是suit中有定义,则该配置会被忽略/覆盖。 | ||
-xmlpathinjar | jar包下xml的路径 | 包含测试jar中有效XML文件的路径(例如“resources/testng.XML”)。默认值是“testng.xml”,这意味着在jar文件的根目录下有一个名为“testng.xml”的文件。除非指定了“-testjar”,不然将忽略此选项。 |
https://testng.org/doc/ant.html
https://testng.org/doc/eclips...
https://testng.org/doc/idea.html
本例建立一个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();
【 https://jitpack.io/com/github... 】
若是testng.xml中的<include>和<exclude>标记不足以知足您的须要,您可使用BeanShell表达式来决定某个测试方法是否应包含在测试运行中。
您能够在<test>标记下指定此表达式:
<test name="BeanShell test"> <method-selectors> <method-selector> <script language="beanshell"><![CDATA[ groups.containsKey("test1") ]]></script> </method-selector> </method-selectors> <!-- ... -->
当在testng.xml中找到script
标记时,testng将忽略当前<test>标记中组和方法的后续<include>和<exclude>,您的BeanShell表达式将是决定是否包含测试方法的惟一方法。
另外有几个地方还须要注意:
它必须返回布尔值。除此约束外,容许任何有效的BeanShell代码(例如,您可能但愿在工做日期间返回true,而在周末期间返回false,这将容许您根据日期以不一样的方式运行测试)。
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.
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; }
详细示例【 https://www.jianshu.com/p/2f9... 】
有几个接口容许您修改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)。
注解 | 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 |
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断言成功!
关于测试报告,技术选型不少种,我选用的是比较简单、好看的external-report插件,配合自定义的监听器实现
备注:targetsurefire-reportsindex.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:UsersdellAppDataRoamingTyporatypora-user-images1579085189960.png)]
对于单元测试,咱们但愿您按照如下几种规则去设计、处理、编码:
测试包的根目录:必须在src/test/java
下[源码构建时会跳过此目录,单元测试框架默认是扫描此目录]
测试包中java
类的包路径:与实际要测试的类,保持一致[ 参考编写流程中的截图]
测试包的java
类名:遵循OrderQueryService.java -> OrderQueryServiceTest.java
规则
测试包的xml
路径:在实际要测试的类的包下,新建xml
包便可,存放各个测试类型testng.xml
测试包的监听器:在实际要测试的类的包下,根据监听器做用范围,新建listerners
包便可,
mvn test
或者运行对整个类的测试案例时,均可以自动运行完全部测试Assert
断言,不容许使用System.out.print
,使用日志log
占位符输出测试信息repository
层的测试,不容许使用mock进行测试,而且保证要有数据回滚机制,不形成脏数据[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LbjdW9PA-1581562336480)(C:Users86151AppDataRoamingTyporatypora-user-images1580782404369.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8hS2NtoM-1581562336481)(C:Users86151AppDataRoamingTyporatypora-user-images1580782525627.png)]
<!--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>
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: 订单查询测试 * @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() { //一、构建空对象 WorkOrderListQueryReqParam checkParam = new WorkOrderListQueryReqParam(); //二、构建残缺参数(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)); //三、构建订单来源是 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)); //四、完整参数 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() { //一、构建空对象 WorkOrderDetailQueryReqParam checkParam = new WorkOrderDetailQueryReqParam(); //二、构建残缺参数(WorkOrderNo) WorkOrderDetailQueryReqParam checkWorkOrderNoParam = new WorkOrderDetailQueryReqParam(); checkWorkOrderNoParam.setRequestSource(99); //四、完整参数 WorkOrderDetailQueryReqParam param = new WorkOrderDetailQueryReqParam(); param.setRequestSource(99); param.setWorkOrderNo("A201801191022356151"); return new Object[][]{ { 1, checkParam }, { 2, checkWorkOrderNoParam }, { 3, param }, }; }
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和它本身的一些注解【架构的案例已经知足】
<?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为准
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2hhy4e4-1581562336483)(C:Users86151AppDataRoamingTyporatypora-user-images1580782613491.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fZjOfE51-1581562336483)(C:Users86151AppDataRoamingTyporatypora-user-images1580782642269.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5THIiuIq-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782705068.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k6C9V2B2-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782761162.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5QUh17b-1581562336485)(C:Users86151AppDataRoamingTyporatypora-user-images1580782774727.png)]
Debug和正常程序同样,可使用debug模式启动测试案例,对其中的某些入参、返回作断点,查看参数、返回
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cR7dgk8C-1581562336486)(C:Users86151AppDataRoamingTyporatypora-user-images1580782877064.png)]
本案例中执行mvn test
的运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQjPjJpq-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783218995.png)]
若是出现结果错误时,须要对Failed tests
中的错误测试,进行修复,直至Build Success
为止
某些状况下,控制台能够看到测试结果,可是对于项目发布,咱们最好仍是从测试结果报告中查看统计信息
咱们能够从两种类型的报告中获取测试案例的运行状况,哪些成功、失败,从而对它们进行修复。
具体查看哪种报告,看我的习惯。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zzZpRQHQ-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783311156.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oQJqiPaa-1581562336487)(C:Users86151AppDataRoamingTyporatypora-user-images1580783343860.png)]
对于某些方法,对数据完整性依赖性较高,且手动构建数据复杂,测试场景须要全面的时候,须要经过测试案例来模拟完整的:入库-查询-修改-删除场景时,那么咱们在运行某些query的测试案例前,须要插入一些数据来支撑其余测试运行,在运行完测试以后,咱们又须要擦除利用完了的数据至此,咱们须要有相应的手段和case来覆盖这些场景
那么在编写测试案例中,对于service、dao层的curd操做,所产生的脏数据问题,咱们须要从两个角度去考虑:
对于本项目中service、dao数据的回滚,能够从如下3个方面考虑:
有两个类须要说明一下:
1.AbstractTestNGSpringContextTests
测试类只有继承了该类才能拥有注入实例Bean的能力,不然注入报错
总结:【适合处理查询的测试案例】
2.AbstractTransactionalTestNGSpringContextTests
测试类继承该类后拥有注入实例能力,同时拥有事物控制能力
总结:【适应于任何场景,推荐使用】
因此,处理本地项目中的service
和dao
,对于数据库产生的数据,咱们只须要将测试类继承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); } //... }
测试类继承AbstractTestNGSpringContextTests
类,搭配使用@Rollback
注解,对部分测试方法进行事物回滚,避免测试案例过程当中的测试数据最后成为了脏数据,运行完测试案例后,TestNG
会自动帮助咱们回滚对应注解了@Rollback方法所产生的数据,其余没有加注解的方法则会真实做用于数据库层面,慎用。
示例:
public class TestNgCurdServiceImplTest extends AbstractTestNGSpringContextTests { //.... //写法保持不变,不须要改动任何代码,只须要在须要回滚的方法上加上注解@Rollback注解便可 //.... }
手动编程,将测试案例运行先后,产生的全部数据,手动调用相关的删除delete接口逐一擦除【适合远程服务】
下面章节【远程服务数据回滚】有相关的案例和说明!
若是一个测试案例以非Mock方式运行,而且有对远程服务进行调用,产生了脏数据,那么此时只能经过编程式回滚数据,即在执行完测试案例的先后,调用相关delete方法,进行清除,若是测试案例上下文较为复杂,对数据的回收分析就变得比较重要,而且服务间链式调用过长,一旦测试案例产生了错误,那么会产生不可预知的一些问题,须要谨慎使用。
本例中,咱们也有相关的case覆盖:
核心流程:
测试远程服务的增删改查,则必然须要在查询方法前,咱们须要插入准备数据,才能测试查询接口,咱们能够经过TestNg
提供的执行机制,在运行完测试案例以后调用相关的delete方法,清除运行期间临时准备的测试数据,不然会污染数据库。
有兴趣的小伙伴能够根据如下代码自行测试一下
/** * 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); }
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); } } }
<?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方法报错,那么插入数据库中的数据就会保留,须要手动去清除库,调用链路若是比较长,涉及面较广的时候,存在不肯定性!
章节【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&characterencoding=utf-8&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脚本,以避免数据混乱,核心属性
Profile
【开发、测试环境】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Goszc2CT-1581562336489)(C:Users86151AppDataRoamingTyporatypora-user-images1580965050323.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 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:Users86151AppDataRoamingTyporatypora-user-images1580965229125.png)]
1.对于本地服务,很显然【自动回滚】的方式最佳,无代码侵入,安全、干净,实现简便。
2.对于远程服务,显然是Mock的方式处理测试案例效果更好,mock测试自己就是一种假定,不会对数据库的数据产生实际影响,设计者只须要关心测试案例的核心业务,而不须要操心因环境、数据所带来的额外问题。
在某些状况下,对于某些深度依赖的bean或者是远程服务bean,并且这些bean或者服务基本能够确保没有问题,只是本地环境有限,没法产生实际的调用时,就可使用mockito对这些bean进行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); } }
【官网】 https://testng.org/doc/docume...