DevOps系列文章之 自动化测试大全(单测和集成测试)

自动化测试业界主流工具

核心目标: 主要是功能测试和覆盖率测试

业界常用主流工具

GoogleTest

GoogleTest是一个跨平台的(Liunx、Mac OS X、Windows 、Cygwin 、Windows CE and Symbian ) C++单元测试框架,由google公司发布,为在不同平台上为编写C++测试而开发的。它提供了丰富的断言、致命和非致命判断、参数化、”死亡测试”等等。例如:

测试用例本身就是一个exe工程,编译之后可以直接运行,非常的方便。

编写测试案例变的非常简单(使用一些简单的宏如TEST),让我们将更多精力花在测试用例设计上。

提供了强大丰富的断言的宏,用于对各种不同检查点的检查。

提高了丰富的命令行参数对脚本运行进行一系列的设置。

pytest

pytest是一个非常成熟的全功能的支持Python语言的单元自动化测试框架。简单灵活,容易上手,支持参数化;能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试,以及接口自动化测试(pytest集成requests)。

Mockito

Mockito是GitHub上使用最广泛的Mock框架,并与JUnit结合使用Mockito框架可以创建和配置mock对象。使用Mockito简化了具有外部依赖的类的测试开发

JMockit

JMockit是一个用于Java语言单元测试的开源Mock工具,包含了工具和API集合。Jmockit可以和junit和TestNG配合使用编写单元测试。

JMockit支持类级别整体mock和部分方法重写,以及实例级别整体mock和部分mock,可以mock静态方法、私有变量及局部方法。

这个工具还具有统计单元测试代码覆盖率的功能,提供了三种类型的代码覆盖率,如行覆盖率、路径覆盖率和数据覆盖率。

Spock

Spock是一个为Groovy和Java语言应用程序来测试和规范的框架。这个框架的突出点在于它美妙和高效表达规范的语言。得益于JUnit Runner,Spock能够在大多数IDE、编译工具、持续集成服务下工作。Spock的灵感源于JUnit,JMock, RSpec, Groovy,,Scala,,Vulcans以及其他优秀的框架形态。

Junit

JUnit是一个为Java编程语言设计的开源单元测试框架,由 Kent Beck 和 Erich Gamma建立,它是单元测试框架家族中的一个,这些框架被统称为xUnit,JUnit是xUnit 家族中最为成功的一个。JUnit 有它自己的 Junit 扩展生态圈,多数 Java 的开发环境都已经集成了 JUnit 作为单元测试的工具。JUnit 的最新版本是JUnit 5,它不再是一个单一的JAR 包,而是由JUnit platform(平台)、JUnit Jupiter 和JUnit Vintage 这3 部分组成。

TestNG

TestNG 是另一个为Java编程语言设计的开源单元测试框架,是一个受JUnit和NUnit启发而来的测试框架,但它引入了一些新功能,使其更强大、更容易使用,例如:

核心特性是多线程测试执行,测试代码是否是多线程安全的;

提供注释支持;

支持数据驱动测试(使用@DataProvider);

支持参数化测试;

强大的执行模型(不再有TestSuite);

支持各种工具和插件(Eclipse, IDEA, Maven等…);

嵌入BeanShell以获得更多的灵活性;

用于运行时和日志记录的默认JDK函数(没有依赖关系)。

Selenium

Selenium也是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE、Mozilla Firefox、Mozilla Suite等。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。

Spring Test

Spring Test是Spring MVC自带了一个非常有用的测试框架,该框架无需进行Web容器即可进行深度测试。

它是用于向Spring应用程序编写自动测试的最有用的库之一。它提供了一流的支持,可以为Spring的应用程序(包括MVC控制器)编写单元测试和集成测试。

自动化测试

随着企业内部自动化资产的持续积累以及持续集成的推进,对自动化测试的运行速度要求越来越高,某些应用的自动化测试用例可能多达几千条,如何加速这些自动化用例的运行速度,让自动化落实到每一次版本变更和持续集成当中,是一个需要解决的问题。

Squaretest

**1,**我使用的是idea,我们先来下载一下插件,File——>Settings——>Plugins,搜索Squaretest,然后install就好了,插件安装完成后需要重启一下

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

**2,**首先我们打开一个类,这个类就是我们即将要作为实验的类,因为Squaretest生成的单元测试方法都是只能生成public的,当然这也是合理的嘛!毕竟private的肯定被public调用了

**3,**如果我们来手写这个类的单元测试,光看都要一会,下面看我操作,打开你的类,光标定位到代码里,右击鼠标选择Generate…

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

**4,**然后你就会看到这里有两个熟悉的图标,第一次的话选择第二个选项,它会让你选择你一下单元测试的模板,因为我已经选择过了,所以我现在演示不回再弹出,但后面我会告诉你怎么更改模板。

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

**5,**选择第二项后就会弹出一个框看下面这里它自动会识别出当前类需要Mock的成员变量,直接点ok

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

**6,**自动会使用类的真实目录层次在test文件夹中创建出来一个单元测试类,类名就是原类名后加Test

在这里插入图片描述

我们只需要检查一下逻辑, 稍微调整下就可以了,代码覆盖率还是很高的。

Mockito

Mock单元测试

添加依赖

<!--mockito依赖-->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.7.19</version>
    <scope>test</scope>
</dependency>
<!-- junit依赖 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
1)Mockito:简单轻量级的做mocking测试的框架;
2)mock对象:在调试期间用来作为真实对象的替代品;
3)mock测试:在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试;
4)stub:打桩,就是为mock对象的方法指定返回值(可抛出异常);
5)verify:行为验证,验证指定方法调用情况(是否被调用,调用次数等);

简单案例

@Test
public void test0() {
    //1、创建mock对象(模拟依赖的对象)
    final List mock = Mockito.mock(List.class);

    //2、使用mock对象(mock对象会对接口或类的方法给出默认实现)
    System.out.println("mock.add result => " + mock.add("first"));  //false
    System.out.println("mock.size result => " + mock.size());       //0

    //3、打桩操作(状态测试:设置该对象指定方法被调用时的返回值)
    Mockito.when(mock.get(0)).thenReturn("second");
    Mockito.doReturn(66).when(mock).size();

    //3、使用mock对象的stub(测试打桩结果)
    System.out.println("mock.get result => " + mock.get(0));    //second
    System.out.println("mock.size result => " + mock.size());   //66

    //4、验证交互 verification(行为测试:验证方法调用情况)
    Mockito.verify(mock).get(Mockito.anyInt());
    Mockito.verify(mock, Mockito.times(2)).size();

    //5、验证返回的结果(这是JUnit的功能)
    assertEquals("second", mock.get(0));
    assertEquals(66, mock.size());
}

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

1、行为验证

• 一旦mock对象被创建了,mock对象会记住所有的交互,然后你就可以选择性的验证你感兴趣的交互,验证不通过则抛出异常。

@Test
public void test1() {
    final List mockList = Mockito.mock(List.class);
    mockList.add("mock1");
    mockList.get(0);
    mockList.size();
    mockList.clear();
    // 验证方法被使用(默认1次)
    Mockito.verify(mockList).add("mock1");
    // 验证方法被使用1次
    Mockito.verify(mockList, Mockito.times(1)).get(0);
    // 验证方法至少被使用1次
    Mockito.verify(mockList, Mockito.atLeast(1)).size();
    // 验证方法没有被使用
    Mockito.verify(mockList, Mockito.never()).contains("mock2");
    // 验证方法至多被使用5次
    Mockito.verify(mockList, Mockito.atMost(5)).clear();
    // 指定方法调用超时时间
    Mockito.verify(mockList, timeout(100)).get(0);
    // 指定时间内需要完成的次数
    Mockito.verify(mockList, timeout(200).atLeastOnce()).size();
}

2、如何做一些测试桩stub

• 默认情况下,所有的函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;

• 一旦测试桩函数被调用,该函数将会一致返回固定的值;

• 对于 static 和 final 方法, Mockito 无法对其 when(…).thenReturn(…) 操作。

@Test
public void test2() {
    //静态导入,减少代码量:import static org.mockito.Mockito.*;
    final ArrayList mockList = mock(ArrayList.class);

    // 设置方法调用返回值
    when(mockList.add("test2")).thenReturn(true);
    doReturn(true).when(mockList).add("test2");
    System.out.println(mockList.add("test2"));  //true

    // 设置方法调用抛出异常
    when(mockList.get(0)).thenThrow(new RuntimeException());
    doThrow(new RuntimeException()).when(mockList).get(0);
    System.out.println(mockList.get(0));    //throw RuntimeException

    // 无返回方法打桩
    doNothing().when(mockList).clear();

    // 为回调做测试桩(对方法返回进行拦截处理)
    final Answer<String> answer = new Answer<String>() {
        @Override
        public String answer(InvocationOnMock invocationOnMock) throws Throwable {
            final List mock = (List) invocationOnMock.getMock();
            return "mock.size result => " + mock.size();
        }
    };
    when(mockList.get(1)).thenAnswer(answer);
    doAnswer(answer).when(mockList).get(1);
    System.out.println(mockList.get(1));    //mock.size result => 0

    // 对同一方法多次打桩,以最后一次为准
    when(mockList.get(2)).thenReturn("test2_1");
    when(mockList.get(2)).thenReturn("test2_2");
    System.out.println(mockList.get(2));    //test2_2
    System.out.println(mockList.get(2));    //test2_2

    // 设置多次调用同类型结果
    when(mockList.get(3)).thenReturn("test2_1", "test2_2");
    when(mockList.get(3)).thenReturn("test2_1").thenReturn("test2_2");
    System.out.println(mockList.get(3));    //test2_1
    System.out.println(mockList.get(3));    //test2_2

    // 为连续调用做测试桩(为同一个函数调用的不同的返回值或异常做测试桩)
    when(mockList.get(4)).thenReturn("test2").thenThrow(new RuntimeException());
    doReturn("test2").doThrow(new RuntimeException()).when(mockList).get(4);
    System.out.println(mockList.get(4));    //test2
    System.out.println(mockList.get(4));    //throw RuntimeException

    // 无打桩方法,返回默认值
    System.out.println(mockList.get(99));    //null
}

3、参数匹配器

• 参数匹配器使验证和测试桩变得更灵活;

• 为了合理的使用复杂的参数匹配,使用equals()与anyX() 的匹配器会使得测试代码更简洁、简单。有时,会迫使你重构代码以使用equals()匹配或者实现equals()函数来帮助你进行测试;

• 如果你使用参数匹配器,所有参数都必须由匹配器提供;

• 支持自定义参数匹配器;

@Test
public void test3() {
    final Map mockMap = mock(Map.class);

    // 正常打桩测试
    when(mockMap.get("key")).thenReturn("value1");
    System.out.println(mockMap.get("key"));     //value1

    // 为灵活起见,可使用参数匹配器
    when(mockMap.get(anyString())).thenReturn("value2");
    System.out.println(mockMap.get(anyString()));   //value2
    System.out.println(mockMap.get("test_key"));    //value2
    System.out.println(mockMap.get(0)); //null

    // 多个入参时,要么都使用参数匹配器,要么都不使用,否则会异常
    when(mockMap.put(anyString(), anyInt())).thenReturn("value3");
    System.out.println(mockMap.put("key3", 3));     //value3
    System.out.println(mockMap.put(anyString(), anyInt()));     //value3
    System.out.println(mockMap.put("key3", anyInt()));    //异常

    // 行为验证时,也支持使用参数匹配器
    verify(mockMap, atLeastOnce()).get(anyString());
    verify(mockMap).put(anyString(), eq(3));

    // 自定义参数匹配器
    final ArgumentMatcher<ArgumentTestRequest> myArgumentMatcher = new ArgumentMatcher<ArgumentTestRequest>() {
        @Override
        public boolean matches(ArgumentTestRequest request) {
            return "name".equals(request.getName()) || "value".equals(request.getValue());
        }
    };
    // 自定义参数匹配器使用
    final ArgumentTestService mock = mock(ArgumentTestService.class);
    when(mock.argumentTestMethod(argThat(myArgumentMatcher))).thenReturn("success");
    doReturn("success").when(mock).argumentTestMethod(argThat(myArgumentMatcher));
    System.out.println(mock.argumentTestMethod(new ArgumentTestRequest("name", "value")));  // success
    System.out.println(mock.argumentTestMethod(new ArgumentTestRequest()));     //null
}

4、执行顺序验证

• 验证执行顺序是非常灵活的-你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可;

• 你可以仅通过那些需要验证顺序的mock对象来创建InOrder对象;

@Test
public void test4() {
    // 验证同一个对象多个方法的执行顺序
    final List mockList = mock(List.class);
    mockList.add("first");
    mockList.add("second");
    final InOrder inOrder = inOrder(mockList);
    inOrder.verify(mockList).add("first");
    inOrder.verify(mockList).add("second");

    // 验证多个对象多个方法的执行顺序
    final List mockList1 = mock(List.class);
    final List mockList2 = mock(List.class);
    mockList1.get(0);
    mockList1.get(1);
    mockList2.get(0);
    mockList1.get(2);
    mockList2.get(1);
    final InOrder inOrder1 = inOrder(mockList1, mockList2);
    inOrder1.verify(mockList1).get(0);
    inOrder1.verify(mockList1).get(2);
    inOrder1.verify(mockList2).get(1);
}

5、确保交互(interaction)操作不会执行在mock对象上

• 一些用户可能会在频繁地使用verifyNoMoreInteractions(),甚至在每个测试函数中都用。但是verifyNoMoreInteractions()并不建议在每个测试函数中都使用;

• verifyNoMoreInteractions()在交互测试套件中只是一个便利的验证,它的作用是当你需要验证是否存在冗余调用时;

@Test
public void test5() {
    // 验证某个交互是否从未被执行
    final List mock = mock(List.class);
    mock.add("first");
    verify(mock, never()).add("test5");   //通过
    verify(mock, never()).add("first");  //异常

    // 验证mock对象没有交互过
    final List mock1 = mock(List.class);
    final List mock2 = mock(List.class);
    verifyZeroInteractions(mock1);  //通过
    verifyNoMoreInteractions(mock1, mock2); //通过
    verifyZeroInteractions(mock, mock2);  //异常

    // 注意:可能只想验证前面的逻辑,但是加上最后一行,会导致出现异常。建议使用方法层面的验证,如:never();
    //      在验证是否有冗余调用的时候,可使用此种方式。如下:
    final List mockList = mock(List.class);
    mockList.add("one");
    mockList.add("two");
    verify(mockList).add("one");    // 通过
    verify(mockList, never()).get(0);    //通过
    verifyZeroInteractions(mockList);   //异常
}

6、使用注解简化mock对象创建

注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中:


MockitoAnnotations.initMocks(this);

也可以使用内置的runner: MockitoJUnitRunner 或者一个rule : MockitoRule;

// 代替 mock(ArgumentTestService.class) 创建mock对象;
@Mock
private ArgumentTestService argumentTestService;
// 若改注解修饰的对象有成员变量,@Mock定义的mock对象会被自动注入;
@InjectMocks
private MockitoAnnotationServiceImpl mockitoAnnotationService;

@Test
public void test6() {
    // 注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中;
    MockitoAnnotations.initMocks(this);
    when(argumentTestService.argumentTestMethod(new ArgumentTestRequest())).thenReturn("success");
    System.out.println(argumentTestService.argumentTestMethod(new ArgumentTestRequest()));  //success
    System.out.println(mockitoAnnotationService.mockitoAnnotationTestMethod()); //null
}

7、监控真实对象(部分mock)

• 可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了;

• 尽量少使用spy对象,使用时也需要小心形式,例如spy对象可以用来处理遗留代码;

• stub语法中同样提供了部分mock的方法,可以调用真实的方法;

完全mock:

上文讲的内容是完全mock,即创建的mock对象与真实对象无关,mock对象的方法默认都是基本的实现,返回基本类型。可基于接口、实现类创建mock对象。

部分mock:

所谓部分mock,即创建的mock对象时基于真实对象的,mock对象的方法都是默认使用真实对象的方法,除非stub之后,才会以stub为准。基于实现类创建mock对象,否则在没有stub的情况下,调用真实方法时,会出现异常。

注意点:

Mockito并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互,不要期望从监控对象得到正确的结果。 当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果

@Test
public void test7() {
    // stub部分mock(stub中使用真实调用)。注意:需要mock实现类,否则会有异常
    final StubTestService stubTestService = mock(StubTestServiceImpl.class);
    when(stubTestService.stubTestMethodA("paramA")).thenCallRealMethod();
    doCallRealMethod().when(stubTestService).stubTestMethodB();
    System.out.println(stubTestService.stubTestMethodA("paramA"));  //stubTestMethodA is called, param = paramA
    System.out.println(stubTestService.stubTestMethodB());  //stubTestMethodB is called
    System.out.println(stubTestService.stubTestMethodC());  //null

    // spy部分mock
    final LinkedList<String> linkedList = new LinkedList();
    final LinkedList spy = spy(linkedList);
    spy.add("one");
    spy.add("two");
    doReturn(100).when(spy).size();
    when(spy.get(0)).thenReturn("one_test");
    System.out.println(spy.size()); //100
    System.out.println(spy.get(0)); //one_test
    System.out.println(spy.get(1)); //two

    // spy可以类比AOP。在spy中,由于默认是调用真实方法,所以第二种写法不等价于第一种写法,不推荐这种写法。
    doReturn("two_test").when(spy).get(2);
    when(spy.get(2)).thenReturn("two_test"); //异常 java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
    System.out.println(spy.get(2));   //two_test

    // spy对象只是真实对象的复制,真实对象的改变不会影响spy对象
    final List<String> arrayList = new ArrayList<>();
    final List<String> spy1 = spy(arrayList);
    spy1.add(0, "one");
    System.out.println(spy1.get(0));    //one
    arrayList.add(0, "list1");
    System.out.println(arrayList.get(0));   //list1
    System.out.println(spy1.get(0));    //one

    // 若对某个方法stub之后,又想调用真实的方法,可以使用reset(spy)
    final ArrayList<String> arrayList1 = new ArrayList<>();
    final ArrayList<String> spy2 = spy(arrayList1);
    doReturn(100).when(spy2).size();
    System.out.println(spy2.size());    //100
    reset(spy2);
    System.out.println(spy2.size());    //0
}

8、@Mock 和 @Spy的使用

@Mock 等价于 Mockito.mock(Object.class);

@Spy 等价于 Mockito.spy(obj);

区分是mock对象还是spy对象:
Mockito.mockingDetails(someObject).isMock();
Mockito.mockingDetails(someObject).isSpy();

@Mock
private StubTestService stubTestService;
@Spy
private StubTestServiceImpl stubTestServiceImpl;
@Spy
private StubTestService stubTestServiceImpl1 = new StubTestServiceImpl();
@Test
public void test8() {
    MockitoAnnotations.initMocks(this);
    // mock对象返回默认
    System.out.println(stubTestService.stubTestMethodB());  //null
    // spy对象调用真实方法
    System.out.println(stubTestServiceImpl.stubTestMethodC());  //stubTestMethodC is called
    System.out.println(stubTestServiceImpl1.stubTestMethodA("spy"));  //stubTestMethodA is called, param = spy

    // 区分是mock对象还是spy对象
    System.out.println(mockingDetails(stubTestService).isMock());   //true
    System.out.println(mockingDetails(stubTestService).isSpy());    //false
    System.out.println(mockingDetails(stubTestServiceImpl).isSpy());    //true
}

9、ArgumentCaptor(参数捕获器)捕获方法参数进行验证。(可代替参数匹配器使用)

• 在某些场景中,不光要对方法的返回值和调用进行验证,同时需要验证一系列交互后所传入方法的参数。那么我们可以用参数捕获器来捕获传入方法的参数进行验证,看它是否符合我们的要求。

ArgumentCaptor介绍

通过ArgumentCaptor对象的forClass(Class

ArgumentCaptor的Api

argument.capture() 捕获方法参数

argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值

argument.getAllValues() 方法进行多次调用后,返回多个参数值

@Test
public void test9() {
    List mock = mock(List.class);
    List mock1 = mock(List.class);
    mock.add("John");
    mock1.add("Brian");
    mock1.add("Jim");
    // 获取方法参数
    ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
    verify(mock).add(argument.capture());
    System.out.println(argument.getValue());    //John

    // 多次调用获取最后一次
    ArgumentCaptor argument1 = ArgumentCaptor.forClass(String.class);
    verify(mock1, times(2)).add(argument1.capture());
    System.out.println(argument1.getValue());    //Jim

    // 获取所有调用参数
    System.out.println(argument1.getAllValues());    //[Brian, Jim]
}

10、简化 ArgumentCaptor 的创建

@Mock
private List<String> captorList;
@Captor
private ArgumentCaptor<String> argumentCaptor;
@Test
public void test10() {
    MockitoAnnotations.initMocks(this);
    captorList.add("cap1");
    captorList.add("cap2");
    System.out.println(captorList.size());
    verify(captorList, atLeastOnce()).add(argumentCaptor.capture());
    System.out.println(argumentCaptor.getAllValues());
}

11、高级特性:自定义验证失败信息

@Test
public void test11() {
    final ArrayList arrayList = mock(ArrayList.class);
    arrayList.add("one");
    arrayList.add("two");

    verify(arrayList, description("size()没有调用")).size();
    // org.mockito.exceptions.base.MockitoAssertionError: size()没有调用

    verify(arrayList, timeout(200).times(3).description("验证失败")).add(anyString());
    //org.mockito.exceptions.base.MockitoAssertionError: 验证失败
}

12、高级特性:修改没有测试桩的调用的默认返回值

• 可以指定策略来创建mock对象的返回值。这是一个高级特性,通常来说,你不需要写这样的测试;

• 它对于遗留系统来说是很有用处的。当你不需要为函数调用打桩时你可以指定一个默认的answer;

@Test
public void test12(){
    // 创建mock对象、使用默认返回
    final ArrayList mockList = mock(ArrayList.class);
    System.out.println(mockList.get(0));    //null

    // 这个实现首先尝试全局配置,如果没有全局配置就会使用默认的回答,它返回0,空集合,null,等等。
    // 参考返回配置:ReturnsEmptyValues
    mock(ArrayList.class, Answers.RETURNS_DEFAULTS);

    // ReturnsSmartNulls首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回SmartNull。
    // 如果最终返回对象,那么会简单返回null。一般用在处理遗留代码。
    // 参考返回配置:ReturnsMoreEmptyValues
    mock(ArrayList.class, Answers.RETURNS_SMART_NULLS);

    // 未stub的方法,会调用真实方法。
    //    注1:存根部分模拟使用时(mock.getSomething ()) .thenReturn (fakeValue)语法将调用的方法。对于部分模拟推荐使用doReturn语法。
    //    注2:如果模拟是序列化反序列化,那么这个Answer将无法理解泛型的元数据。
    mock(ArrayList.class, Answers.CALLS_REAL_METHODS);

    // 深度stub,用于嵌套对象的mock。参考:https://www.cnblogs.com/Ming8006/p/6297333.html
    mock(ArrayList.class, Answers.RETURNS_DEEP_STUBS);

    // ReturnsMocks首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回mock。
    // 如果返回类型不能mocked(例如是final)然后返回null。
    mock(ArrayList.class, Answers.RETURNS_MOCKS);

    //  mock对象的方法调用后,可以返回自己(类似builder模式)
    mock(ArrayList.class, Answers.RETURNS_SELF);

    // 自定义返回
    final Answer<String> answer = new Answer<String>() {
        @Override
        public String answer(InvocationOnMock invocation) throws Throwable {
            return "test_answer";
        }
    };
    final ArrayList mockList1 = mock(ArrayList.class, answer);
    System.out.println(mockList1.get(0));   //test_answer
}

三、学习了这么多,牛刀小试一下!

测试实体类

@Data
public class User {

    /**
     * 姓名,登录密码
     */
    

持久层DAO

public interface UserDao {

    /**
     * 根据name查找user
     * @param name
     * @return
     */
    User getUserByName(String name);

    /**
     * 保存user
     * @param user
     * @return
     */
    Integer saveUser(User user);
}

业务层Service接口

public interface UserService {

    /**
     * 根据name查找user
     * @param name
     * @return
     */
    User getUserByName(String name);

    /**
     * 保存user
     * @param user
     * @return
     */
    Integer saveUser(User user);
}

业务层Serive实现类

@Service
public class UserServiceImpl implements UserService {

    //userDao
    @Autowired
    private UserDao userDao;

    /**
     * 根据name查找user
     * @param name
     * @return
     */
    @Override
    public User getUserByName(String name) {
        try {
            return userDao.getUserByName(name);
        } catch (Exception e) {
            throw new RuntimeException("查询user异常");
        }
    }

    /**
     * 保存user
     * @param user
     * @return
     */
    @Override
    public Integer saveUser(User user) {
        if (userDao.getUserByName(user.getName()) != null) {
            throw new RuntimeException("用户名已存在");
        }
        try {
            return userDao.saveUser(user);
        } catch (Exception e) {
            throw new RuntimeException("保存用户异常");
        }
    }
}

现在我们的Service写好了,想要单元测试一下,但是Dao是其他人开发的,目前还没有写好,那我们如何测试呢?

public class UserServiceTest {

    /**
     * Mock测试:根据name查询user
     */
    @Test
    public void getUserByNameTest() {
        // mock对象
        final UserDao userDao = mock(UserDao.class);
        final UserServiceImpl userService = new UserServiceImpl();
        userService.setUserDao(userDao);

        // stub调用
        final User user = new User();
        user.setName("admin");
        user.setPassword("pass");
        when(userDao.getUserByName("admin")).thenReturn(user);

        // 执行待测试方法
        final User user1 = userService.getUserByName("admin");
        System.out.println("查询结果:" + JacksonUtil.obj2json(user1));  //查询结果:{"name":"admin","password":"pass"}

        // 验证mock对象交互
        verify(userDao).getUserByName(anyString());

        // 验证查询结果
        Assert.assertNotNull("查询结果为空!", user1);
        Assert.assertEquals("查询结果错误!", "admin", user1.getName());
    }


    /**
     * Mock测试:保存user
     */
    @Mock
    private UserDao userDao;
    @InjectMocks
    private UserServiceImpl userService;
    @Test
    public void saveUserTest() throws Exception{
        // 执行注解初始化
        MockitoAnnotations.initMocks(this);

        // mock对象stub操作
        final User user = new User();
        user.setName("admin");
        user.setPassword("pass");
        when(userDao.getUserByName("admin")).thenReturn(user).thenReturn(null);
        when(userDao.saveUser(any(User.class))).thenReturn(1);

        // 验证用户名重复的情况
        try {
            userService.saveUser(user);
            throw new Exception();  //走到这里说明验证失败
        } catch (RuntimeException e) {
            System.out.println("重复用户名保存失败-测试通过");   //重复用户名保存失败-测试通过
        }
        verify(userDao).getUserByName("admin");

        // 验证正常保存的情况
        user.setName("user");
        final Integer integer = userService.saveUser(user);
        System.out.println("保存结果:" + integer);  //保存结果:1
        Assert.assertEquals("保存失败!", 1, integer.longValue());

        verify(userDao).saveUser(any(User.class));
        verify(userDao, times(2)).getUserByName(anyString());
    }

}

根据以上代码我们可以知道,当我们的待测类开发完成而依赖的类的实现还没有开发完成。此时,我们就可以用到我们的Mock测试,模拟我们依赖类的返回值,使我们的待测类与依赖类解耦。这样,我们就可以对我们的待测类进行单元测了。

四、参考文档及进一步学习~

Mockito英文版javadoc:https://javadoc.io/static/org.mockito/mockito-core/3.3.3/org/mockito/Mockito.html
Mockito中文文档(部分):https://blog.csdn.net/bboyfeiyu/article/details/52127551#35

Mockito使用教程:https://www.cnblogs.com/Ming8006/p/6297333.html
参数捕获器使用:https://www.journaldev.com/21892/mockito-argumentcaptor-captor-annotation
利用ArgumentCaptor(参数捕获器)捕获方法参数进行验证:https://www.iteye.com/blog/hotdog-916364
改变mock返回值:https://www.huangyunkun.com/2014/10/25/mockito-deep-stub-with-enum/
五分钟了解Mockito:https://www.iteye.com/blog/liuzhijun-1512780
使用Mockito进行单元测试:https://www.iteye.com/blog/qiuguo0205-1443344
JUnit + Mockito 单元测试:https://blog.csdn.net/zhangxin09/article/details/42422643
Mockito中@Mock@InjectMock:https://www.cnblogs.com/langren1992/p/9681600.html
mockito中两种部分mock的实现,spy、callRealMethod:https://www.cnblogs.com/softidea/p/4204389.html
Mockito 中被 Mocked 的对象属性及方法的默认值:https://www.cnblogs.com/fnlingnzb-learner/p/10635250.html
单元测试工具之Mockito:https://blog.csdn.net/qq_32140971/article/details/90598454
引入Mockito测试用@Spy@Mock:https://blog.csdn.net/message_lx/article/details/83308114
Mockito初探(含实例):https://www.iteye.com/blog/sgq0085-2031319
测试覆盖率统计:https://blog.csdn.net/lvyuan1234/article/details/82836052?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
测试覆盖率无法统计解决:https://blog.csdn.net/zhanglei082319/article/details/81536398

Java-faker

单元测试构造数据非常费时费力,多留意一些帮助测试的库,能够极大提交效率。

Java的单元测试经常需要构造各种测试数据,其中一项就是构造测试的字符串。

如果我们想要随机构造人名、地名、天气、学校、颜色、职业,甚至符合某正则表达式的字符串等,肿么办?

那么有一个库叫 java-fake 可以实现这个功能。

maven中添加pom依赖

<dependency>
    <groupId>com.github.javafaker</groupId>
    <artifactId>javafaker</artifactId>
    <version>1.0.0</version>
</dependency>

基本用法

Faker faker = new Faker();

String name = faker.name().fullName(); // Miss Samanta Schmidt
String firstName = faker.name().firstName(); // Emory
String lastName = faker.name().lastName(); // Barton

String streetAddress = faker.address().streetAddress(); // 60018 Sawayn Brooks Suite 449

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

该框架支持多种语言,默认是英文。

可以通过此代码指定语言:

Faker faker = new Faker(new Locale(“YOUR_LOCALE”));
如果是中文:zh-CN

在这里插入图片描述

支持正则

 @Test
    public void bothifyShouldGenerateLettersAndNumbers() {
        assertThat(faker.bothify("????##@gmail.com"), matchesRegularExpression("\\w{4}\\d{2}@gmail.com"));
    }
 
    @Test
    public void letterifyShouldGenerateLetters() {
        assertThat(faker.bothify("????"), matchesRegularExpression("\\w{4}"));
    }
 
    @Test
    public void letterifyShouldGenerateUpperCaseLetters() {
        assertThat(faker.bothify("????",true), matchesRegularExpression("[A-Z]{4}"));
    }
 
    @Test
    public void letterifyShouldLeaveNonSpecialCharactersAlone() {
        assertThat(faker.bothify("ABC????DEF"), matchesRegularExpression("ABC\\w{4}DEF"));
    }

此框架虽然可以构造各种字符串,但是构造整个复杂对象或者集合就有些力不从心,这时就需要另外一个强大的工具:easy-random https://github.com/j-easy/easy-random

easy-random

一两行就可以构造一个非常复杂的对象或者对象列表。

Java项目写单元测试时,需要构造复杂对象,非常耗时,而且无用的代码很长非常不优雅。

这个工具主要是为了Mock对象,省时省力,结合Mockito(可以mock方法)堪称完美。

  1. 添加依赖
<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-random-core</artifactId>
    <version>4.0.0</version>
</dependency>

正常情况要构造Person对象,如果通过构造方法要这么写:

Street street = new Street(12, (byte) 1, "Oxford street");
Address address = new Address(street, "123456", "London", "United Kingdom");
Person person = new Person("Foo", "Bar", "foo.bar@gmail.com", Gender.MALE, address);

如果通过get set方法写:

Street street = new Street();
street.setNumber(12);
street.setType((byte) 1);
street.setName("Oxford street");
 
Address address = new Address();
address.setStreet(street);
address.setZipCode("123456");
address.setCity("London");
address.setCountry("United Kingdom");
 
Person person = new Person();
person.setFirstName("Foo");
person.setLastName("Bar");
person.setEmail("foo.bar@gmail.com");
person.setGender(Gender.MALE);
person.setAddress(address);

而使用easy-random只需要一行:

public class Person {
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }

    private String name;
    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

我们的第一个示例生成一个简单的随机Person对象,该对象没有嵌套对象,集合,只有一个Integer和两个String。

让我们使用nextObject(Class < T > t)生成对象的一个实例:

Person person = new EasyRandom().nextObject(Person.class)

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

如我们所见,生成的字符串可能有点太长,并且年龄是负数。 我们将在后续部分中展示如何进行调整。

其他用法:

可以产生一个int数组。

@org.junit.Test
public void testSortAlgorithm() {
   EasyRandom easyRandom = new EasyRandom();
   // Given
   int[] ints = easyRandom.nextObject(int[].class);

}

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

可以生成一个Person对象,插入到数据库中。

@Test
public void testPersistPerson() throws Exception {
   // Given
   Person person = easyRandom.nextObject(Person.class);

   // When
   personDao.persist(person);

   // Then
   assertThat("person_table").column("name").value().isEqualTo(person.getName()); // assretj db
}

对象集合

现在,我们需要一个Person对象的集合。 另一个方法objects(Class < T > t,int size)将允许我们这样做。

一件好事是,它返回对象流,因此最终,我们可以根据需要向它或组添加中间操作。

这是我们如何生成Person的五个实例的方法:

@Test
void givenDefaultConfiguration_thenGenerateObjectsList() {
    EasyRandom generator = new EasyRandom();
    List<Person> persons = generator.objects(Person.class, 5)
        .collect(Collectors.toList());

    assertEquals(5, persons.size());
}

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

复杂对象生成

让我们看一下Employee类:

@Data
public class Employee {
    private long id;
    private String firstName;
    private String lastName;
    private Department department;
    private Collection<Employee> coworkers;
    private Map<YearQuarter, Grade> quarterGrades;
}

我们的类相对复杂,它具有一个嵌套的对象,一个集合和一个映射。

现在默认情况下,集合的生成范围是1到100,因此我们的Collection 大小将介于两者之间。

一件好事是,对象将被缓存并重新使用,因此不一定所有对象都是唯一的。 不过,我们可能不需要那么多。

我们将很快研究如何调整集合的范围,但是首先,让我们看看我们可能遇到的另一个问题。

在我们的域中,我们有一个YearQuarter类,它代表一年的四分之一。

将endDate设置为精确指向开始日期后的3个月是有逻辑的:

@Data
public class YearQuarter {

    private LocalDate startDate;
    private LocalDate endDate;

    public YearQuarter(LocalDate startDate) {
        this.startDate = startDate;
        autoAdjustEndDate();
    }

    private void autoAdjustEndDate() {
        endDate = startDate.plusMonths(3L);
    }
}

我们必须注意,EasyRandom使用反射来构造我们的对象,因此通过库生成该对象将导致数据很可能对我们没有用,因为我们三个月的约束将无法保留。

让我们看看如何解决这个问题。

在以下配置中,我们通过EasyRandomParameters提供我们的自定义配置。

首先,我们明确说明所需的字符串长度和集合大小。 接下来,我们从生成中排除了某些字段,假设我们有一个仅包含null的原因。

在这里,我们使用了方便的FieldPredicates实用程序来链接排除谓词。

之后,我们通过另一个方便的TypePredicates实用程序将Java包中的所有内容从" not.existing.pkg"中排除。

最后,如所承诺的,我们通过应用customYearQuarterRandomizer解决了有关YearQuarter类的startDate和endDate生成的问题:

public class YearQuarterRandomizer implements Randomizer<YearQuarter> {

    @Override
    public YearQuarter getRandomValue() {
        return new YearQuarter(LocalDate.now());
    }
}
   @Test
    void givenCustomConfiguration_thenGenerateSingleEmployee() {
        EasyRandomParameters parameters = new EasyRandomParameters();
        parameters.stringLengthRange(3, 3);
        parameters.collectionSizeRange(5, 5);
        parameters.excludeField(FieldPredicates.named("lastName").and(FieldPredicates.inClass(Employee.class)));
        parameters.excludeType(TypePredicates.inPackage("not.existing.pkg"));
        parameters.randomize(YearQuarter.class, new YearQuarterRandomizer());

        EasyRandom generator = new EasyRandom(parameters);
        Employee employee = generator.nextObject(Employee.class);

        assertEquals(3, employee.getFirstName().length());
        assertEquals(5, employee.getCoworkers().size());
        assertEquals(5, employee.getQuarterGrades().size());
        assertNotNull(employee.getDepartment());

        assertNull(employee.getLastName());

        for (YearQuarter key : employee.getQuarterGrades().keySet()) {
            assertEquals(key.getStartDate(), key.getEndDate().minusMonths(3L));
        }
    }

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

Postman Mock

1.Mock简介

1.1 Mock定义

Mock是一种比较特殊的测试技巧,可以在没有依赖项的情况下进行接口或单元测试。通常情况下,Mock与其他方法的区别是,用于模拟代码依赖对象,并允许设置对应的期望值。简单一点来讲,就是Mock创建了一个对象,模拟真实对象的行为。

1.2 Mock目的

因项目中任务的不同分工,会出现每个人的任务进度不一样的情况。就会出现模块A开发完成,但其依赖项模块B还未完成,这时候如果进行集成测试时,就会出现两个模块无法有效完成工作。针对这种情况,Mock服务便应运而生。Postman中的Mock服务器可以减轻团队开发中这种不同步的情况。

1.3 Mock意义

在API开发的前期,构建Mock集合可以帮助团队之间进行清晰有效沟通,并尽快就预期结果达成一致。在实际开发过程中,所有人员可以同步并行工作,减少因相互依赖而导致延期的风险。

1.4 Mock服务

Mock不是一个真实的服务,仅是一个被伪装成真实服务的假服务。通过Mock,可以测试我们API并检验结果是否正确。

Postman可以创建两种类型的Mock服务

  • 私有Mock

私有Mock服务需要在请求头中添加Postman API key,如X-Api-Key:postman API key

  • 公有Mock

公有Mocke服务可以被任何人访问,在使用过程中不需要添加Postman API key

img

2. Postman 创建Mock服务

2.1 创建Mock服务

  • 方法一:通过菜单创建

img

  • 方法二:通过左侧任务栏创建

img

2.2 Postman创建Mock服务

2.2.1 Postman 创建Mock服务器参数
  • Request Method:请求方法

HTTP请求方法,如GET、POST、PUT等

  • Request URL

Mock服务器地址

  • Response Code:

Mock服务器请求成功后返回的状态码

  • Response Body:

Mock服务器返回的消息体

2.2.2 Postman创建Mock步骤
  • 1.在左侧点击Mock Servers,点击Create Mock Server,在右侧填写相应的Mock服务器参数,并点击Next,如下所示:

img

  • 2.填写Mock服务器的相关信息并点击Create Mock Server,如下所示:

img

  • 3.在创建Mock Server成功后,会出现如下界面:

img

3.访问Postman Mock服务

切换至Collections,发送请求,如下所示:

  • GET请求Mock示例

img

  • POST请求Mock示例

img

TestNG

TestNG是Java中的一个测试框架, 类似于JUnit 和NUnit, 功能都差不多, 只是功能更加强大,使用也更方便。

Java中已经有一个JUnit的测试框架了,TestNG比JUnit功能强大的多, 测试人员一般用TestNG来写自动化测试,开发人员一般用JUnit写单元测试。

官方网站: http://testng.org/doc/index.html

核心特点: TestNG 中的多线程使用姿势

使用场景:

当测试回归用例集里包含了大量此类的用例时,如果还用传统的单线程执行方式,则一次自动化回归将会耗费大量的时间。

基于上述场景,我们可以考虑将自动化用例中相互之间没有耦合关系,相对独立的用例进行并行执行。如,我可以通过起不同的线程同时去执行不同的 MR 任务、Spark 任务,每个线程各自负责跟踪任务的执行情况。

此外,即使是单纯的接口自动化测试,如果测试集里包含了大量的用例时,我们也可以借助于 TestNG 的多线程方式提高执行速度。

必须要指出的是,通过多线程执行用例时虽然可以大大提升用例的执行效率,但是我们在设计用例时也要考虑到这些用例是否适合并发执行,以及要注意多线程方式的通病:线程安全与共享变量的问题。建议是在测试代码中,尽可能地避免使用共享变量。如果真的用到了,要慎用 synchronized 关键字来对共享变量进行加锁同步。否则,难免你的用例执行时可能会出现不稳定的情景(经常听到有人提到用例执行地不稳定,有时 100% 通过,有时只有 90% 通过,猜测可能有一部分原因也是这个导致的)。

TestNG基本运用和运行

1、一个简单的例子如下
import org.testng.Assert;
import org.testng.annotations.Test;

public class Case {
    @Test
    public void test(){
        System.out.println("this is testng case");
        Assert.assertTrue(true);
    }
}
直接用脚本方式运行:右键Run

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

xml方式运行:新建testng.xml,右键Run
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="Suite1" verbose="1" >
    <test name="Nopackage" >
        <classes>
            <class name="com.tech.suc.Case" />
        </classes>
    </test>
</suite>

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

TestNG注解说明

注解描述
@BeforeSuite在该套件的所有测试都运行在注释的方法之前,仅运行一次
@AfterSuite在该套件的所有测试都运行在注释方法之后,仅运行一次
@BeforeClass在调用当前类的第一个测试方法之前运行,注释方法仅运行一次
@AfterClass在调用当前类的第一个测试方法之后运行,注释方法仅运行一次
@BeforeTest注释的方法将在属于test标签内的类的所有测试方法运行之前运行
@AfterTest注释的方法将在属于test标签内的类的所有测试方法运行之后运行
@BeforeGroups配置方法将在之前运行组列表。 此方法保证在调用属于这些组中的任何一个的第一个测试方法之前不久运行
@AfterGroups此配置方法将在之后运行组列表。该方法保证在调用属于任何这些组的最后一个测试方法之后不久运行
@BeforeMethod注释方法将在每个测试方法之前运行
@AfterMethod注释方法将在每个测试方法之后运行
@DataProvider标记一种方法来提供测试方法的数据。 注释方法必须返回一个Object [] [],其中每个Object []可以被分配给测试方法的参数列表。 要从该DataProvider接收数据的@Test方法需要使用与此注释名称相等的dataProvider名称
@Factory将一个方法标记为工厂,返回TestNG将被用作测试类的对象。 该方法必须返回Object []
@Listeners定义测试类上的侦听器
@Parameters描述如何将参数传递给@Test方法
@Test将类或方法标记为测试的一部分,此标记若放在类上,则该类所有公共方法都将被作为测试方法
常用注解简单例子

import org.testng.annotations.Test;

public class Case {
    @Test(enabled = false)
    public void Test1(){
        System.out.println("忽略测试,此测试用例不会运行");
    }

    //“超时”表示如果单元测试花费的时间超过指定的毫秒数,
    // 那么TestNG将会中止它并将其标记为失败。此项常用于性能测试
    @Test(timeOut = 5000)
    public void Test2(){
        System.out.println("超时测试");
    }

    @Test(description = "Test3用例描述信息")
    public void Test3(){
        System.out.println("描述,用于展示在测试报告中");
    }
    //依赖测试用例
    @Test(dependsOnMethods = {"Test2"})
    public void Test4(){
        System.out.println("依赖测试,执行该用例前会先执行Test1用例");
    }
    //依赖测试用例分组
    @Test(dependsOnGroups = {"group1"})
    public void Test5(){
        System.out.println("依赖测试,执行该用例前会先执行Test1用例");
    }

    @Test(groups = "group1")
    public void Test6(){
        System.out.println("分组测试1,Test6");
    }
    @Test(groups = "group1")
    public void Test7(){
        System.out.println("分组测试1,Test7");
    }

    @Test(groups = "group2")
    public void Test8(){
        System.out.println("分组测试group2,在xml执行时可以体现");
    }
}

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

其他说明

依赖测试

TestNG允许指定依赖关系:

  • 在@Test注释中使用属性dependsOnMethods
  • 在@Test注释中使用属性dependsOnGroups

除此之外依赖还分为hard依赖和soft依赖:

  • hard依赖:

    默认为此依赖方式,

    即其所有依赖的methods或者groups必须全部pass

    ,否则被标识依赖的类或者方法将会被略过,在报告中标识为skip,如后面的范例所示,此为默认的依赖方式;

    • 以下实例的被依赖方法Test4的结果为fail:因为所依赖的method(Test1)不执行,即fail,所以会导致被依赖(Test4)fail。若Test1是true,则Test4执行成功。
    
    import org.testng.annotations.Test;
    
    public class Case {
        @Test(enabled = false)
        public void Test1(){
            System.out.println("忽略测试,此测试用例不会运行");
        }
        //依赖测试用例
        @Test(dependsOnMethods = {"Test1"})
        public void Test4(){
            System.out.println("依赖测试,执行该用例前会先执行Test1用例");
        }
    }
    

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

  • **soft依赖:**此方式下,其依赖的方法或者组有不是全部pass也不会影响被标识依赖的类或者方法的运行,注意如果使用此方式,则依赖者和被依赖者之间必须不存在成功失败的因果关系,否则会导致用例失败。此方法在注解中需要加入alwaysRun=true即可,如@Test(dependsOnMethods= {“TestNgLearn1”}, alwaysRun=true);
XML中的并发
测试代码:TestMethods.java
import org.testng.Assert;
import org.testng.Reporter;
import org.testng.annotations.Test;

public class TestMethods {

    @Test
    public void test1(){
        Assert.assertEquals(2,2);
        System.out.println("this is thread"+ Thread.currentThread().getId());
    }

    @Test
    public void test2(){
        Assert.assertEquals(1,1);
        System.out.println("this is thread"+ Thread.currentThread().getId());
    }

    @Test
    public void test3(){
        Assert.assertEquals("sss","sss");
        System.out.println("this is thread"+ Thread.currentThread().getId());
    }

}
xml设置:tests级别并发示例

testng.xml

<?xml version="1.0" encoding="UTF-8" ?>

<suite name="Suite1" parallel="tests" thread-count="2">
    <test name="测试用例1">
        <classes>
            <class name="com.test.extent.TestMethods"></class>
        </classes>
    </test>
    <test name="测试用例2">
        <classes>
            <class name="com.test.extent.TestMethods"></class>
        </classes>
    </test>
    <test name="测试用例3">
        <classes>
            <class name="com.test.extent.TestMethods"></class>
        </classes>
    </test>
</suite>

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

说明:在当前测试规划的执行过程中,为每个测试用例(指的是xml中的)的执行使用单独的线程(该测试用例中的测试方法共享一个线程),

最多并发2个线程,执行结果如下图:的3个方法的线程id均为12

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

xml设置:classes级别并发示例
<?xml version="1.0" encoding="UTF-8" ?>

<suite name="Suite1" parallel="classes" thread-count="2">
    <test name="测试用例1">
        <classes>
            <class name="com.test.extent.TestMethods"></class>
            <class name="com.test.extent.TestMethods2"></class>
        </classes>
    </test>
</suite>

说明:在当前测试规划的执行过程中,为每个测试类(指的是xml中的)的执行使用单独的线程(该测试类中的测试方法共享一个线程),最多并发2个线程。执行结果如下图:

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

xml设置:methods级别并发示例
<?xml version="1.0" encoding="UTF-8" ?>

<suite name="Suite1" parallel="methods" thread-count="2">
    <test name="test1">
        <classes>
            <class name="com.test.extent.TestMethods"></class>
        </classes>
    </test>
</suite>

说明:在当前测试规划的执行过程中,为每个测试方法(指的是每个@Test)的执行使用单独的线程,最多并发2个线程,执行结果如下图:

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

不同级别的并发测试

通常,在 TestNG 的执行中,测试的级别由上至下可以分为suite -> test -> class -> method,箭头的左边元素跟右边元素的关系是一对多的包含关系。

这里的 test 指的是 testng.xml 中的 test tag,而不是测试类里的一个*@*Test。测试类里的一个*@*Test实际上对应这里的 method。所以我们在使用*@*BeforeSuite*@*BeforeTest*@*BeforeClass*@*BeforeMethod这些标签的时候,它们的实际执行顺序也是按照这个级别来的。

suite

一般情况下,一个 testng.xml 只包含一个 suite。如果想起多个线程执行不同的 suite,官方给出的方法是:通过命令行的方式来指定线程池的容量。

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

即可通过三个线程来分别执行 testng1.xml、testng2.xml、testng3.xml。
实际上这种情况在实际中应用地并不多见,我们的测试用例往往放在一个 suite 中,如果真需要执行不同的 suite,往往也是在不同的环境中去执行,届时也自然而然会做一些其他的配置(如环境变量)更改,会有不同的进程去执行。因此这种方式不多赘述。

test, class, method

test,class,method 级别的并发,可以通过在 testng.xml 中的 suite tag 下设置,如:

<suite name="Testng Parallel Test" parallel="tests" thread-count="5">
<suite name="Testng Parallel Test" parallel="classes" thread-count="5">
<suite name="Testng Parallel Test" parallel="methods" thread-count="5">

它们的共同点都是最多起 5 个线程去同时执行不同的用例。
它们的区别如下:

  • tests 级别:不同 test tag 下的用例可以在不同的线程执行,相同 test tag 下的用例只能在同一个线程中执行。
  • classs 级别:不同 class tag 下的用例可以在不同的线程执行,相同 class tag 下的用例只能在同一个线程中执行。
  • methods 级别:所有用例都可以在不同的线程去执行。

搞清楚并发的级别非常重要,可以帮我们合理地组织用例,比如将非线程安全的测试类或 group 统一放到一个 test 中,这样在并发的同时又可以保证这些类里的用例是单线程执行。也可以根据需要设定 class 级别的并发,让同一个测试类里的用例在同一个线程中执行。

并发时的依赖

实践中,很多时候我们在测试类中通过 dependOnMethods/dependOnGroups 方式,给很多测试方法的执行添加了依赖,以达到期望的执行顺序。如果同时在运行 testng 时配置了 methods 级别并发执行,那么这些测试方法在不同线程中执行,还会遵循依赖的执行顺序吗?答案是——YES。牛逼的 TestNG 就是能在多线程情况下依然遵循既定的用例执行顺序去执行。

不同 dataprovider 的并发

在使用 TestNG 做自动化测试时,基本上大家都会使用 dataprovider 来管理一个用例的不同测试数据。而上述在 testng.xml 中修改 suite 标签的方法,并不适用于 dataprovider 多组测试数据之间的并发。执行时会发现,一个 dp 中的多组数据依然是顺序执行。

解决方式是:在*@*DataProvider中添加 parallel=true。
如:

import org.testng.annotations.DataProvider;
import testdata.ScenarioTestData;


public class ScenarioDataProvider {
    @DataProvider(name = "hadoopTest", parallel=true)
    public static Object [][] hadoopTest(){
        return new Object[][]{
            ScenarioTestData.hadoopMain,
            ScenarioTestData.hadoopRun,
            ScenarioTestData.hadoopDeliverProps
        };
    }

    @DataProvider(name = "sparkTest", parallel=true)
    public static Object [][] sparkTest(){
        return new Object[][]{
            ScenarioTestData.spark_java_version_default,
            ScenarioTestData.spark_java_version_162,
            ScenarioTestData.spark_java_version_200,
            ScenarioTestData.spark_python
        };
    }

    @DataProvider(name = "sqoopTest", parallel=true)
    public static Object [][] sqoopTest(){
        return new Object[][]{
            ScenarioTestData.sqoop_mysql2hive,
            ScenarioTestData.sqoop_mysql2hdfs
        };
    }
}

默认情况下,dp 并行执行的线程池容量为 10,如果要更改并发的数量,也可以在 suite tag 下指定参数 data-provider-thread-count:

<suite name="Testng Parallel Test" parallel="methods" thread-count="5" data-provider-thread-count="20" >
同一个方法的并发

有些时候,我们需要对一个测试用例,比如一个 http 接口,执行并发测试,即一个接口的反复调用。TestNG 中也提供了优雅的支持方式,在*@*Test标签中指定 threadPoolSize 和 invocationCount。

@Test(enabled=true, dataProvider="testdp", threadPoolSize=5, invocationCount=10)

其中 threadPoolSize 表明用于调用该方法的线程池容量,该例就是同时起 5 个线程并行执行该方法;invocationCount 表示该方法总计需要被执行的次数。该例子中 5 个线程同时执行,当总计执行次数达到 10 次时,停止。

注意,该线程池与 dp 的并发线程池是两个独立的线程池。这里的线程池是用于起多个 method,而每个 method 的测试数据由 dp 提供,如果这边 dp 里有 3 组数据,那么实际上 10 次执行,每次都会调 3 次接口,这个接口被调用的总次数是 10*3=30 次。threadPoolSize 指定的 5 个线程中,每个线程单独去调 method 时,用到的 dp 如果也是支持并发执行的话,会创建一个新的线程池(dpThreadPool)来并发执行测试数据。

示例代码如下:

package testng.parallel.test;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;


public class TestClass1 {
    private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
    @BeforeClass
    public void beforeClass(){
        System.out.println("Start Time: " + df.format(new Date()));
    }

    @Test(enabled=true, dataProvider="testdp", threadPoolSize=2, invocationCount=5)
    public void test(String dpNumber) throws InterruptedException{
        System.out.println("Current Thread Id: " + Thread.currentThread().getId() + ". Dataprovider number: "+ dpNumber);
        Thread.sleep(5000);
    }

    @DataProvider(name = "testdp", parallel = true)
    public static Object[][]testdp(){
        return new Object[][]{
            {"1"},
            {"2"}
        };
    }

    @AfterClass
    public void afterClass(){
        System.out.println("End Time: " + df.format(new Date()));
    }
}

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

测试结果:

Start Time: 2017-03-11 14:10:43
[ThreadUtil] Starting executor timeOut:0ms workers:5 threadPoolSize:2
Current Thread Id: 14. Dataprovider number: 2
Current Thread Id: 15. Dataprovider number: 2
Current Thread Id: 12. Dataprovider number: 1
Current Thread Id: 13. Dataprovider number: 1
Current Thread Id: 16. Dataprovider number: 1
Current Thread Id: 18. Dataprovider number: 1
Current Thread Id: 17. Dataprovider number: 2
Current Thread Id: 19. Dataprovider number: 2
Current Thread Id: 21. Dataprovider number: 2
Current Thread Id: 20. Dataprovider number: 1
End Time: 2017-03-11 14:10:58

参数化测试

在自动化测试项目中,参数化是必不可少的,以下将会介绍TestNG中的参数化测试运用。

TestNG可以通过两种不同的方式将参数直接传递给测试方法:

  • 使用testng.xml
  • 使用数据提供者
使用testng.xml传送参数
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

public class Case {
    @Test
    @Parameters({"username","password"})
    public void Login(String username, String password){
        System.out.println("您的用户名是:"+username);
        System.out.println("您的密码是:"+password);
    }
}
xml配置如下
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="Suite1" verbose="1" >
    <test name="param">
        <parameter name="username" value="李白"></parameter>
        <parameter name="password" value="123456"></parameter>
        <classes>
            <class name="com.tech.design.Case"></class>
        </classes>
    </test>
</suite>
运行xml,结果如下

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

使用@DataProvider传递参数

如果需要传递复杂的参数或java创建的对象(复杂对象、从属性文件或数据库读取数据等),通过testng.xml文件的方式可能不太适合。这种情况可以使用数据驱动的方式为你的测试提供数据。数据库驱动是在类中定义一个方法,返回一组数据对象。该方法使用@DataProvider注解。

此处需要注意,传参的类型必须要一致,且带有@DataProvider注解的函数返回的必然是Object[][],此处需要注意。

通过DataProvider,返回值分别是Object[][]和Iterator<Object[]>

1、返回值是Object[][],代码如下:
import org.testng.annotations.DataProvider;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

public class Case {

    @DataProvider(name = "data")
    public Object[][] provideData(){
        return new Object[][] { {"李白",100},{"杜甫",110},{"鲁班",210} };
    }
  //若调用者和提供在不同的类,则写法如下  //@Test(dataProvider = "data",dataProviderClass = Case.class)
    @Test(dataProvider = "data")
    public void Test(String par1, int par2){
        System.out.println("姓名:"+par1+","+"年龄:"+par2);
    }

}

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

2、返回值是Iterator<Object[]>,代码如下
import org.testng.annotations.DataProvider;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

import java.util.HashSet;
import java.util.Iterator;

public class Case {
    @DataProvider(name="testdp")
    public static Iterator<Object[]> createData() {
        HashSet<Integer> set = new HashSet<Integer>();
        set.add(Integer.valueOf(4));
        set.add(Integer.valueOf(5));

        HashSet<Object[]> so = new HashSet<Object[]>();
        for(Integer intg:set){
            so.add(new Object[]{intg});
        }

        return so.iterator();
    }

    @Test(dataProvider="testdp")
    public void login(Integer caseNum) {
        System.out.println(caseNum);
    }

}

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

3、dataProvider方法可接受 的参数:Method method, ITestContext context

参数可以传其中一个,也可以两个一起传入,这里只演示Method参数,

用处:当多个测试方法使用同一个@DataProvider提供的测试数据,并希望不同的测试方法返回不同的值时,这是很有用的方式

示例:定义1个数据提供方法和2个测试方法。

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.lang.reflect.Method;
public class Case {
    @DataProvider(name = "datapro")
    public Object[][] Data(Method method){
        System.out.println("当前测试方法为:"+method.getName());
        Object[] S1 = new Object[]{"李白"};
        Object[] S2 = new Object[]{"张楚"};
        if(method.getName().equals("Test2")){
            return new Object[][] {S1};
        }else {
            return new Object[][] {S2};
        }
    }

    @Test(dataProvider = "datapro")
    public void Test1(String s){
        System.out.println(s);
    }
    @Test(dataProvider = "datapro")
    public void Test2(String s){
        System.out.println(s);
    }

}

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

Allure测试报告

Allure框架是一个灵活轻量级多语言测试报告工具,它不仅可以以WEB的方式展示简介的测试结果,而且允许参与开发过程的每个人从日常执行的测试中最大限度的提取有用信息。
    Allure报告简化了常见缺陷的统计:失败的测试可以分为BUG和被中断的测试,还可以配置日志、步骤、fixture、附件、计时、执行历史以及与BUG管理系统集成,所以,通过以上配置,所有负责的开发人员和测试人员可以尽可能的掌握测试信息。

Allure安装

本次用maven项目演示,直接用pom自动更新下载依赖包(allure),具体代码如下:

Windows安装allure工具
1、安装jdk配置环境变量
2、下载allure安装包并配置环境变量

  • .下载Allure命令行,下载地址如下所示:
https://github.com/allure-framework/allure2/releases/
  • 将下载到本地的allure压缩包解压到指定目录,并添加相应的环境变量
Path=D:\Program Files\Allure\allure-2.16.0\bin

在命令行中输入allure,不出现报错即可

Usage: allure [options] [command] [command options]
  Options:
    --help
      Print commandline help.
    -q, --quiet
      Switch on the quiet mode.
      Default: false
      ...
allure执行

1、用pom.xml拉好依赖包后,执行测试用例

2、测试用例执行完成后会在项目目录下生成:allure-results文件夹

3、最后通过IDEA 的Terminal功能,在当前项目下执行如下命令

  • allure generate allure-results #生成一个指定的报告到指定位置 (默认生成allure-report文件)
  • allure generate allure-results -o allure-report --clean #清空已有测试报告后再生成
  • allure open allure-report #打开生成的报告

maven项目为例说如何使用

添加pom依赖与allure插件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>6.14.3</version>
        </dependency>
        <!-- 依赖reportNg 关联testNg-->
        <dependency>
            <groupId>org.uncommons</groupId>
            <artifactId>reportng</artifactId>
            <version>1.1.4</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.testng</groupId>
                    <artifactId>testng</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--allure的testng插件-->
        <dependency>
            <groupId>ru.yandex.qatools.allure</groupId>
            <artifactId>allure-testng-adaptor</artifactId>
            <version>1.3.6</version>
            <exclusions>
                <exclusion>
                    <groupId>org.testng</groupId>
                    <artifactId>testng</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-testng</artifactId>
            <version>2.17.0</version>
        </dependency>
        <!-- 依赖Guice -->
        <dependency>
            <groupId>com.google.inject</groupId>
            <artifactId>guice</artifactId>
            <version>4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.20</version>
                <configuration>
                    <argLine>
                        -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
                    </argLine>
                    <!--生成allure-result的目录-->
                    <systemProperties>
                        <property>
                            <name>allure.results.directory</name>
                            <value>./target/allure-results</value>
                        </property>
                    </systemProperties>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.aspectj</groupId>
                        <artifactId>aspectjweaver</artifactId>
                        <version>${aspectj.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

</project>

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

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

重新生成使用 allure generate allure-results -o allure-report --clean

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

执行 allure open allure-report 打开 查看测试报告。

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

在这里插入图片描述

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

Other TestNG Tips

TestNG 作为一个成熟的、业界广泛使用的测试框架,自然有其存在的合理性。这边再分享一些简单有用的标签,具体的使用姿势大家可以自己去探索,官网有比较全的介绍,毕竟自己探索的才会印象深刻。

  1. groups/dependsOnGroups/dependsOnMethods ——设置用例间依赖
  2. dataProviderClass ——将 dataprovider 单独放到一个专用的类中,实现测试代码、dataprovider、测试数据分层。
  3. timeout ——设置用例的超时时间(并发/非并发都可支持)
  4. alwaysRun ——某些依赖的用例失败了,导致用例被跳过。对于一些为了保持环境干净而 “扫尾” 的测试类,如果我们想强制执行可以使用此标签。
  5. priority ——设置优先级,让某些测试用例被更大概率优先执行。
  6. singleThreaded ——强制一个 class 类里的用例在一个线程执行,忽视 method 级别并发
  7. preserve-order ——指定是否按照 testng.xml 中的既定用例顺序执行用例

总结

在 TestNG 中使用多线程的方式并行执行测试用例可以有效提供用例的执行速度,而且 TestNG 对多线程提供了很好的支持,即使是菜鸟也可以方便地上手多线程。此外,TestNG 默认会使用线程池的方式创建线程,减小了程序的开销。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder_Boy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值