最近在写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 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,则已经说明代码设计上是存在问题的。
就是使用代码重构中的 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 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 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 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));
}
}