最近在写UnitTest代码的时候遇到了点小问题,在解决过程当中有了点小心得,做一下记录。
完整代码在此处下载: https://download.csdn.net/download/zxm317122667/10652780
问题主要对于 private
方法的单元测试。
比如如下代码:有两个类,分别是Fish
和Rod
鱼Fish.java
package com.example.dannyjiang.testprivatedemo;
public class Fish {
// 某一条鱼Fish的唯一标识
public long id;
// Fish的X坐标
private int x;
// Fish的Y坐标
private int y;
// Fish的大小,默认为100
public int size = 100;
public Fish() {
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
鱼钩Rod.java
package com.example.dannyjiang.testprivatedemo;
public class Rod {
// Rod的唯一标识
public long id;
// Rod的X坐标
private int x;
// Rod的Y坐标
private int y;
// Fish的大小,默认为100
private int size = 100;
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getSize() {
return size;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
以及一个用来判断一个Rod是否钓到了一条鱼Fish的封装类Controller.java
package com.example.dannyjiang.testprivatedemo;
import java.util.List;
public class Controller {
/**
* 传入一个List,判断在此List中,是否有与Rod匹配的Fish对象
* @param fishList
* @param rod
*/
public boolean hasFishMatched(List<Fish> fishList, Rod rod) {
for (Fish fish : fishList) {
// 需要判断Fish是否与Rod重叠
if (fishOverlapWithRod(fish, rod)) {
System.out.println(String.format("fish %d has been matched", fish.id));
return true;
}
}
System.out.println("no fish found");
return false;
}
/**
* 判断Fish是否与某一个Rod有重叠
* @param fish
* @param rod
* @return
*/
private boolean fishOverlapWithRod(Fish fish, Rod rod) {
return fish.getX() < rod.getX() + rod.getSize()
&& fish.getX() + fish.size > rod.getX()
&& fish.getY() < rod.getY() + rod.getSize()
&& fish.getY() + fish.size > rod.getY();
}
}
可以看到在Controller
类中提供了一个public
的方法给外部调用。但是在这个方法中还调用了一个private
的fishOverlapWithRod
方法。这样就造成很难去给hasFishMatched
写单元测试代码。在网上查过很多资料如何去给一个private
的方法写单元测试。网上主要介绍了有几种框架,例如:PowerMOckito
、JMockit
、或者微软的MSTest
。但是一次很偶然的机会看到了Practical Object Oriented Design in Ruby
这本书的作者Sandi Metz
写的一句话:
The solution to the problem of costly tests, however, Getting good value from tests requires
clarity of intention and knowing what, when, and how to test.
才发现如果想对一个private
的方法去写UnitTest,则已经说明代码设计上是存在问题的。
问题主要是如下两点:
- 违反了类的单一职责原则(SRP)
- 这种写法属于Code Smell中的特性嫉妒(Feature envy)或者是数据簇
解决思路
就是使用代码重构中的 Extract Class 将判断Fish
和Rod
是否重叠的代码抽象到Fish
中。 修改后的代码如下:
Fish.java
package com.example.dannyjiang.testprivatedemo;
public class Fish {
// 某一条鱼Fish的唯一标识
public long id;
// Fish的X坐标
private int x;
// Fish的Y坐标
private int y;
// Fish的大小,默认为100
public int size = 100;
public Fish() {
}
public boolean overlap(Rod rod) {
if (rod == null) {
throw new RuntimeException("rod is null");
}
return x < rod.getX() + rod.getSize()
&& x + size > rod.getX()
&& y < rod.getY() + rod.getSize()
&& y + size > rod.getY();
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
可以看待在Fish.java
中多了一个overlap
方法专门用来判断它是否与传入的Rod
重叠。 同时Rod.java
不需要做其它的修改。这样修改后,Controller.java
中只要改为如下即可:
package com.example.dannyjiang.testprivatedemo;
import java.util.List;
public class Controller {
/**
* 传入一个List,判断在此List中,是否有与Rod匹配的Fish对象
* @param fishList
* @param rod
*/
public boolean hasFishMatched(List<Fish> fishList, Rod rod) {
for (Fish fish : fishList) {
// 需要判断Fish是否与Rod重叠
if (fish.overlap(rod)) {
System.out.println(String.format("fish %d has been matched", fish.id));
return true;
}
}
System.out.println("no fish found");
return false;
}
}
单元测试
最后分别对Controller.java
和Fish.java
书写UnitTest代码即可实现功能代码的覆盖率,代码如下:
ControllerTest.java
package com.example.dannyjiang.testprivatedemo;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ControllerTest {
@Test
public void no_fish_match1() {
Controller controller = new Controller();
List<Fish> fishList = new ArrayList<>();
Fish fish1 = mock(Fish.class);
Fish fish2 = mock(Fish.class);
Fish fish3 = mock(Fish.class);
fishList.add(fish1);
fishList.add(fish2);
fishList.add(fish3);
Rod rod = mock(Rod.class);
when(fish1.overlap(rod)).thenReturn(false);
when(fish2.overlap(rod)).thenReturn(false);
when(fish3.overlap(rod)).thenReturn(false);
assertFalse(controller.hasFishMatched(fishList, rod));
}
@Test
public void has_fish_match1() {
Controller controller = new Controller();
List<Fish> fishList = new ArrayList<>();
Fish fish1 = mock(Fish.class);
Fish fish2 = mock(Fish.class);
Fish fish3 = mock(Fish.class);
fishList.add(fish1);
fishList.add(fish2);
fishList.add(fish3);
Rod rod = mock(Rod.class);
when(fish1.overlap(rod)).thenReturn(false);
when(fish2.overlap(rod)).thenReturn(true);
when(fish3.overlap(rod)).thenReturn(false);
assertFalse(controller.hasFishMatched(fishList, rod));
}
}
FishTest.java
package com.example.dannyjiang.testprivatedemo;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FishTest {
private Fish fish = null;
@Before
public void setUp() {
fish = new Fish();
fish.id = 1;
fish.setX(0);
fish.setY(0);
fish.size = 100;
}
@Test(expected = RuntimeException.class)
public void process_fish_with_null() {
fish.overlap(null);
}
@Test
public void process_fish_not_overlapped() {
Rod mockRod = mock(Rod.class);
when(mockRod.getX()).thenReturn(101);
when(mockRod.getY()).thenReturn(101);
when(mockRod.getSize()).thenReturn(100);
assertFalse(fish.overlap(mockRod));
}
@Test
public void process_fish_overlapped() {
Rod mockRod = mock(Rod.class);
when(mockRod.getX()).thenReturn(50);
when(mockRod.getY()).thenReturn(50);
when(mockRod.getSize()).thenReturn(100);
assertTrue(fish.overlap(mockRod));
}
}