在使用Junit进行单元测试时,有时候会遇到测试方法返回数组、对象、容器的情况,如果直接使用assertEquals()方法,大多数情况并不能得到断言结果,这时候该如何判断测试的期望值与实际值是否相等呢?
本文的测试环境为JDK8+JUnit5
先说结论:断言判断数组使用assertArrayEquals()方法。
被测试类的方法如下:
public class MyClass {
//静态方法,返回值为数组
public static int[] arryMethod() {
return new int[] {0,1,2};
}
}
为了测试方便,方法为静态方法,返回一个值为{0, 1, 2}的数组。
Junit测试代码如下:
class TestMain {
@Test
void test() {
//错误
//assertEquals(new int[] {0, 1, 2}, MyClass.arryMethod());
assertArrayEquals(new int[] {0, 1, 2}, MyClass.arryMethod());
}
}
运行test()方法,你会发现assertArrayEquals()通过,而==assertEquals()==方法测试不通过。
assertArrayEquals()用于判断两个数组是否相等,包括对象数组(对象数组与数组对象一定要能区分开来)。
接下来将对assertEquals()断言未通过的原因进行分析,以及介绍assertEquals()与assertArrayEquals()方法的源码,发现它们的异同。
assertEquals的测试情况如下所示:
从Failure Trace中很明显可以看到,assertEquals()方法是在比较两个数组对象(没错,数组也是对象!一个比较特殊的对象,它的父类是Object,如果不理解为什么数组是对象可以查看文末参考链接),而非数组的值,所以断言未通过。
(1) 为什么会输出I@402a079呢?
因为数组对象未重写Object的toString()方法,Object的toString方法默认返回对象的名称及引用地址:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
(2) assertEquals()方法既然没有比较数组的值,那它是如何比较两个数组的?
答案是:先比较两个数组对象的引用,不等则调用equals()方法进行比较
理解数组是对象这一点十分重要,assertEquals()方法将传入的数组视为Object对象进行处理,JUnit源码调用顺序如下:
assertEquals(new int[] {0, 1, 2}, MyClass.arryMethod());
========================================================
//第一步
static void assertEquals(Object expected, Object actual) {
assertEquals(expected, actual, (String) null);
}
//第二步
static void assertEquals(Object expected, Object actual, String message) {
if (!objectsAreEqual(expected, actual)) {
failNotEqual(expected, actual, message);
}
}
//第三步
static boolean objectsAreEqual(Object obj1, Object obj2) {
if (obj1 == null) {
return (obj2 == null);
}
return obj1.equals(obj2);
}
先判断两个对象的引用是否相等,如果不相等,调用对象的equals()方法。
而Object.equals()方法默认比较两个对象的引用是否相等。
public boolean equals(Object obj) {
return (this == obj);
}
(3) 那么assertArrayEquals()又是如何进行比较的呢?
既然assertArrayEquals()方法比较的是数组的值,应该可以猜到,它会去遍历数组进行一一比较。那到底是不是呢?我们来看看源码:
assertArrayEquals(new int[] {0, 1, 2}, MyClass.arryMethod());
=============================================================
//第一步
static void assertArrayEquals(int[] expected, int[] actual) {
assertArrayEquals(expected, actual, (String) null);
}
//第二步
static void assertArrayEquals(int[] expected, int[] actual, String message) {
assertArrayEquals(expected, actual, null, message);
}
//第三步
private static void assertArrayEquals(int[] expected, int[] actual,
Deque<Integer> indexes,
Object messageOrSupplier) {
if (expected == actual) {
return;
}
assertArraysNotNull(expected, actual, indexes, messageOrSupplier);
assertArraysHaveSameLength(expected.length, actual.length,
indexes, messageOrSupplier);
for (int i = 0; i < expected.length; i++) {
if (expected[i] != actual[i]) {
failArraysNotEqual(expected[i], actual[i],
nullSafeIndexes(indexes, i),
messageOrSupplier);
}
}
}
可以看到,assertArrayEquals()方法先比较两个数组对象的引用是否相等,再判断是否为NULL,以及是否长度相等,最后再遍历数组,一一比较两个数组的值。
(4) 总结
assertEquals()方法针对的是基本数据类型、单一对象间的断言
assertArrayEquals()方法针对的是数组对象间的断言
基本类型数组对象
Object型数组对象(感兴趣的同学可以自己试着查看对应源码)
通过1.2的分析,我们明确了assertEquals()方法针对的是基本数据类型、单一对象间的断言,所以断言对象相等使用assertEquals()方法。
这里我们写一个自定义类Student以用作测试,
public class Student {
public String name;
public Integer age;
public Student(String name, Integer age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age == null) {
if (other.age != null)
return false;
} else if (!age.equals(other.age))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
Student类包含有name、age属性,有参构造方法、覆盖了Object父类的toString()方法和equals()方法。
被测试类的方法如下:
public class MyClass {
public static Student stuMethod() {
return new Student("Rhine", 1);
}
}
通过调用stuMethod()方法返回一个Student对象,以模拟我们的被测试方法。
Junit测试代码如下:
class TestMain {
@Test
void test() {
//断言失败,只比较对象的引用是否相等
//assertSame(new Student("Rhine",1), MyClass.stuMethod());
assertEquals(new Student("Rhine",1), MyClass.stuMethod());
}
}
根据1.2中源码的分析,我们重写了Student类的equals()方法(使用Eclipse自动生成的),这样assertEquals()方法就能够成功通过。
当我们注释掉equals()方法,自然断言就不能通过了。
说到equals()方法,就不得不与==、hashCode()方法一起来讨论了,这篇文章已经介绍得很清楚了。
List、Map、Set等容器也是我们经常用来作为方法返回值,那怎样去断言容器是否相等呢?
我们可以换一个思路去实现对容器判定是否相等。
被测试类的方法如下:
public class MyClass {
public static ArrayList<Object> listMethod() {
ArrayList<Object> list = new ArrayList<Object>();
list.add(1);
list.add(2);
list.add(3);
return list;
}
public static HashMap<String, Object> mapMethod(){
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
return map;
}
public static HashSet<Object> setMethod() {
HashSet<Object> set = new HashSet<Object>();
set.add(1);
set.add(2);
set.add(3);
return set;
}
}
分别用于返回我们常用的容器类型。
Junit测试代码如下:
class TestMain {
@Test
void test() {
ArrayList<Object> list = new ArrayList<Object>();
list.add(1);
list.add(2);
list.add(3);
assertTrue(list.equals(MyClass.listMethod()));
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
assertTrue(map.equals(MyClass.mapMethod()));
HashSet<Object> set = new HashSet<Object>();
set.add(1);
set.add(2);
set.add(3);
assertTrue(set.equals(set));
}
}
对了,没错!就是使用assertTrue()断言,利用容器重写过后的equals()方法,就能直接判断两个容器是否相同了。
或许你还对equals()方法真的能够判断两个容器相等保持怀疑,我们就看选一个ArrayList的equals()方法源码来看看(ArrayList是实现的AbstractList抽象类,所以它的equals()方法是在AbstractList抽象类中实现的)。
AbstractList.class
==================================
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}
equals()方法先判断两个对象的引用是否相等,再判断是否是List类型,最后通过while()循环,同时遍历两个容器,分别取得其中的对象o1、o2,注意if条件语句里是一个取反的三目运算,以保证两个容器的长度以及所有元素时相等的(整个while循环写得既简洁又优雅),如果容器是等长的那么return语句中的表达式则是true。
另外几个容器有兴趣的同学可以自行查看equals()方法的源码。
理解数组对象:从JVM、反射的角度进行了解释,很具体了。
Object源码介绍:源码配上了少量的介绍。