前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

来源:阿里技术

1、测试环节的简单分类

⬤单元测试:快速地检查一个类或极小范围内的功能

⬤冒烟测试:快速检查系统核心功能是否有明确缺陷

⬤ 集成测试:检查整个应用或系统的功能

⬤ 回归测试:检查变更代码是否破坏旧的功能

⬤ 黑盒测试:将整个系统看成一个黑盒进行不特定测试

⬤ 白盒测试:按照编码或运行细节,设计特定的测试过程

1.1、如何推进

从不写单元测试、不会写单元测试,到写单元测试、写有效的单元测试,可以从4个阶段来推进。

image-20220329205359078

2、Junit

2.1、mvn testmvn surefire:test

surefire:testmaven-surefire-plugin 中定义的一个任务,默认绑定在 test 阶段运行

⬤ 当运行 mvn tes t命令时,先运行 test 阶段之前的 compiletest-compilephase 及绑定 goal 任务;

⬤ 而 mvn surefire:test 则是直接运行这个任务,不会执行编译,因此需要提前手动编译好源代码和测试代码;

Maven 会自动收集当前项目的所有模块,做依赖树和插件合并。当 pom 中未声明任何插件或者插件版本号为空,Maven 会使用默认值进行填充。

mavendefault 生命周期和插件版本关系的声明文件:maven-core-3.6.3.jar/META-INF/plexus/default-bindings.xml

2.2、JUnit4 是如何被 Maven 唤起的

前文提到,surefire 插件的会将测试任务绑定在 test 阶段,因此当运行 mvn test时会调用 surefire 插件的方法。surefire 通过 SPI 机制扫描类路径下,发现测试引擎实现类(需要实现 org.apache.maven.surefire.providerapi.SurefireProvider),从而将测试任务转嫁到具体的执行引擎

2.2.1、按测试类名称过滤

Maven Surefire 插件无其他测试框架的依赖注入时,默认使用 JUnit3Provider 作为执行引擎,因此要求测试类命名为以下模式:

过程中会排除所有嵌套类(包括静态成员类),也可以通过在 pom 文件中配置include和exclude规则来覆盖默认行为。


**/Test*.java
**/*Test.java
**/*Tests.java
**/*TestCase.java

2.2.2、默认引擎下扫描测试方法的规则:

⬤ 测试方法必须是 public, 非 static,返回类型为 void,无参的方法;

⬤ 测试方法必须写成 testXxx形式;

⬤ 全局变量可以在无参的构造方法中初始化;

⬤ 每次执行一个测试用例前,执行一遍setUp(),用于对数据的初始化;执行完一个测试用例后,再执行tearDown(),用于销毁还原数据;

因此当我们需要使用 JUnit4 的注解如 @Before,需要添加依赖 surefire-junit4 告诉 SurefirePlugin 优先使用 JUnit4@Runner 来运行。

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.16</version>
  <dependencies>
    <dependency>
      <groupId>org.apache.maven.surefire</groupId>
      <artifactId>surefire-junit4</artifactId>
      <version>2.16</version>
    </dependency>
  </dependencies>
</plugin>

3、MOCK编程

单元测试中,一个重要原则就是不扩大测试范围,尽可能将 mock 外部依赖,例如外部的 RPC 服务、数据库等中间件。被 mock 的对象可以称作。

两大目的

1.验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等

2.指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

3.1、测试替身分类

「测试替身」,它来源于电影中的特技替身的概念。Meszaros 在他的文中[2]定义了五类替身。

image-20220329212107753

3.2、Mockito

3.2.1、mock出一个虚假的对象

@Test
public void test_1() {
  PersonDTO person = Mockito.mock(PersonDTO.class);

}

3.2.2、验证方法调用没(关心参数)&次数

@Test
public void test2_1() {
  PersonDTO person = Mockito.mock(PersonDTO.class);

  // 1、验证person的getSex得到了调用
  person.getSex(1);
  Mockito.verify(person).getSex(1);
  Mockito.verify(person, Mockito.times(1)).getSex(1);

}

方法 说明
times(n) 方法被调用n次
never() 没有被调用
atLeast(n) 至少被调用n次
atLeastOnce() 至少被调用1次,相当于atLeast(1)
atMost() 最多被调用n次

3.2.3、验证方法调用没(不关系参数)

@Test
public void test_3() {
  PersonDTO person = Mockito.mock(PersonDTO.class);
  person.printing("healerjean");
  // 1、只关心打印方法走没走,而不关心他的参数是什么的时候,我们就要用到Mock的any方法
  Mockito.verify(person).printing(Mockito.anyString());
}

方法 说明
anyString() 表示任何一个字符串都可以
anyInt  
anyLong  
anyDouble  
anyObject 表示任何对象
any(clazz) 表示任何属于clazz的对象
anyCollection  
anyCollectionOf(clazz)  
anyList(Map, set)  
anyListOf(clazz)  

3.2.4、验证调用顺序

@Test
public void test2_2(){
  PersonDTO person = Mockito.mock(PersonDTO.class);
  person.getSex(1);
  person.isMan(1);
  
  InOrder inOrder = Mockito.inOrder(person);
  inOrder.verify(person).getSex(1);
  inOrder.verify(person).isMan(1);
}

3.2.5、指定某个方法的返回值,或者是执行特定的动作

@Test
public void test_4_1() {
  PersonDTO person = Mockito.mock(PersonDTO.class);

  // 4.1、当调用person的isMan方法,同时传入"0"时,返回true
  // (注意这个时候我们调用person.isMan(0);的时候值为true而调用其他数字则为false,
  //   如果我们忽略数字,传任何值都返回true时,就可以用到我们上面讲的any()参数适配方法)
  Mockito.when(person.isMan(0)).thenReturn(true);
  // true
  System.out.println(person.isMan(0));
  // false
  System.out.println(person.isMan(1));

  //当调用person的isMan方法,同时传入"0"时,返回false,其他默认也都是 false
  Mockito.when(person.isMan(0)).thenReturn(false);
  // false
  System.out.println(person.isMan(0));
  // false
  System.out.println(person.isMan(1));


  Mockito.when(person.isMan(Mockito.anyInt())).thenReturn(true);
  // true
  System.out.println(person.isMan(0));
  // true
  System.out.println(person.isMan(1));
}

3.2.6、指定某法方法抛出异常

@Test
public void test_4_2() {
  List list = Mockito.mock(List.class);
  list.add("123");
  //1、当list调用clear()方法时会抛出异常
  Mockito.doThrow(new RuntimeException()).when(list).clear();
  list.clear();
}

3.2.7、指定返回特定值

public void test_4_3() {
  List list = Mockito.mock(List.class);
  Mockito.doReturn("123").when(list).get(Mockito.anyInt());
  System.out.println(list.get(0));
}

3.2.8、mock对象默认不调用&真实调用

@Test
public void test_4_4(){
  Foo foo = Mockito.mock(Foo.class);

  //1、什么信息也不会打印, mock对象并不会调用真实逻辑
  foo.doFoo();

  //2、啥也不会打印出来
  Mockito.doNothing().when(foo).doFoo();
  foo.doFoo();
  //不会调用真实逻辑,但是int默认值就是0,所以打印0
  // 打印0
  System.out.println(foo.getCount());

  //3、这里会调用真实逻辑, 打印出信息
  Mockito.doCallRealMethod().when(foo).doFoo();
  // 打印:"method doFoo called."
  foo.doFoo();

  Mockito.doCallRealMethod().when(foo).getCount();
  // 打印 0
  System.out.println(foo.getCount());

}

class Foo {
  public void doFoo() {
    System.out.println("method doFoo called.");
  }
  public int getCount() {
    return 1;
  }
}

3.2.9、@Mock注解

需要配合 @RunWith(MockitoJUnitRunner.class) 注解使用。

1、对该对象所有非私有方法的调用都没有调用 真实方法

2、对该对象私有方法的调用无法进行模拟,会调用 真实方法

//和 List list = Mockito.mock(List.class);作用一致
@Mock
private List list;
@Test
public void test_5(){
  list.add("one");
  Mockito.verify(list).add("one");
}

3.2.10、@SPY

@Mock相同点:spymock 生成的对象不受 spring 管理

spy 默认会调用真实的方法,有返回值的返回真实的返回值,而mock 默认不执行,有返回值的,默认返回null,日常测试中我们往往只需要Mock一个对象中的某些方法,而非全部,因此@Spy更便于我们做Mock测试

@Spy
List<String> spiedList = new ArrayList<>();
@Test
public void test_6(){
  spiedList.add("one");
  spiedList.add("two");

  // 打印 one
  System.out.println(spiedList.get(0));
  Mockito.verify(spiedList).add("one");
  Mockito.verify(spiedList).add("two");

  //输出 2
  System.out.println(spiedList.size());

  Mockito.doReturn(100).when(spiedList).size();
  //输出 100
  System.out.println(spiedList.size());

}

3.2.11、@InjectMocks

3.2.11.1、不用该注解

@Repository
public class MyRepository {
 
    public void doSomething() {
        System.out.println("here's dosomething");
    }
 
    public Model findById(Long id) {
        return new Model(id, "Real Repository");
    }
}
@Service
public class MyService {

  @Autowired
  private MyRepository myRepository;

  public void doSomething() {
    this.myRepository.doSomething();
  }

  public Model findById(Long id) {
    return this.myRepository.findById(id);
  }
}
MyRepository myRepository = Mockito.mock(MyRepository.class); 
MyService myService = new MyService(myRepository);

3.2.11.2、使用该注解

@Autowird 等方式完成自动注入。在单元测试中,没有启动 spring 框架,此时就需要通过 @ InjectMocks完成依赖注入。@InjectMocks会将带有@Spy@Mock 注解的对象尝试注入到被 测试的目标类中

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

  @Mock
  private MyRepository myRepository;

  @InjectMocks
  private MyService myService;

  @Test
  public void testInjectMocks() {
    System.out.println(myService.getMyRepository().getClass());
  }
}

如果我们还有一个 MyController 如下,需要注入 MyService 应该怎么解决呢?

@Controller
public class MyController {

  @Autowired
  private MyService myService;

  public void doSomething() {
    this.myService.doSomething();
  }

  public Model findById(Long id) {
    return this.myService.findById(id);
  }
}

解决如下

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:beans.xml"})
public class MyControllerTest {

  @Mock
  private MyRepository myRepository;

  @InjectMocks
  @Autowired
  private MyService myService;

  @Autowired
  private MyController myController;

  @Before
  public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
    Model model = new Model(11L, "AAA");
    doNothing().when(myRepository).doSomething();
    when(myRepository.findById(11L)).thenReturn(model);
  }

  @Test
  public void doSomething() throws Exception {
    this.myController.doSomething();
  }

  @Test
  public void findById() throws Exception {
    System.out.println(this.myController.findById(11L));
  }
}

ContactAuthor