项目经验_之_单元测试
前言
Github:https://github.com/HealerJean
来源:阿里技术
1、测试环节的简单分类
⬤单元测试:快速地检查一个类或极小范围内的功能
⬤冒烟测试:快速检查系统核心功能是否有明确缺陷
⬤ 集成测试:检查整个应用或系统的功能
⬤ 回归测试:检查变更代码是否破坏旧的功能
⬤ 黑盒测试:将整个系统看成一个黑盒进行不特定测试
⬤ 白盒测试:按照编码或运行细节,设计特定的测试过程
1.1、如何推进
从不写单元测试、不会写单元测试,到写单元测试、写有效的单元测试,可以从4个阶段来推进。
2、Junit
2.1、mvn test
和 mvn surefire:test
surefire:test
是maven-surefire-plugin
中定义的一个任务,默认绑定在test
阶段运行⬤ 当运行
mvn tes
t命令时,先运行test
阶段之前的compile
、test-compile
等phase
及绑定goal
任务;⬤ 而
mvn surefire:test
则是直接运行这个任务,不会执行编译,因此需要提前手动编译好源代码和测试代码;
Maven
会自动收集当前项目的所有模块,做依赖树和插件合并。当pom
中未声明任何插件或者插件版本号为空,Maven
会使用默认值进行填充。
maven
的default
生命周期和插件版本关系的声明文件:maven-core-3.6.3.jar/META-INF/plexus/default-bindings.xml
。
2.2、JUnit4
是如何被 Maven
唤起的
前文提到,
surefire
插件的会将测试任务绑定在test
阶段,因此当运行mvn tes
t时会调用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]定义了五类替身。
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
相同点:spy
和mock
生成的对象不受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));
}
}