其他
单元测试实践篇:Mock
这类应用的单元测试不能像微服务化的应用一样,可以方便的将整个 service 在本地 Run Test,但是依靠于日常开发部署环境的远程 debug、日志、Arthas 等工具定位项目自测联调中的问题又会显得格外的笨重,问题修复几秒钟,发布一次 10min 会成为严重的效率瓶颈。
如何高效的自测代码逻辑,如何不启动整个服务就能验证我的目标方法呢?那就是我今天要介绍的三板斧 Mockito + PowerMock + AssertJ
▐ 配置 Maven 依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<!-- use 2.9.1 for Java 7 projects -->
<version>3.17.1</version>
<scope>test</scope>
</dependency>
▐ Mockito
import java.util.concurrent.TimeUnit;
public class MockTarget {
public void soSth() {
System.out.println("do sth.");
}
public String sayHello() {
return "Hello";
}
public String sayHello(String greetings) {
return "Hello " + greetings;
}
public String callMethod(Object p) {
return "callMethod " + p.toString();
}
public String callMethodWait(long million) {
try {
TimeUnit.MILLISECONDS.sleep(million);
} catch (InterruptedException ignored) {
}
return "callMethod sleep " + million;
}
public Object callMethodWithException(Object p) {
throw new IllegalStateException("测试异常");
}
}
when..then
通过 doCallRealMethod 指定 mock 对象的方法调用它的真实逻辑,也可通过 thenAnswer(Answers.CALLS_REAL_METHODS) 实现 通过 when..thenThrow 或者 doThrow..when 的方式 mock 目标方法返回对应的异常 通过 AssertJ 的句法 assertThatExceptionOfType..isThrownBy..withXxx断言某个方法的执行会抛出预期异常 anyXxx() 可用于表示任意类型的任意参数
anyString() 代表任意字符串
anyInt() 代表任意int数值
anyObject() 代表任意类型对象
@Test
public void testWhenAndThen() {
MockTarget mock = Mockito.mock(MockTarget.class);
when(mock.sayHello()).thenReturn("mock hello");
assertEquals(mock.sayHello(), "mock hello");
doCallRealMethod().when(mock).sayHello();
assertEquals(mock.sayHello(), "Hello");
when(mock.sayHello(anyString())).thenAnswer(Answers.CALLS_REAL_METHODS);
assertEquals(mock.sayHello("testRun"), "Hello testRun");
when(mock.callMethod(any())).thenReturn("mock return");
assertEquals(mock.callMethod(new Object()), "mock return");
when(mock.callMethodWithException(any())).thenThrow(new RuntimeException("mock throw exception"), new IllegalArgumentException("test illegal argument"));
Assertions.assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> mock.callMethodWithException("first invoke"))
.withMessage("mock throw exception");
Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> mock.callMethodWithException("second invoke"))
.withMessage("test illegal argument")
.withNoCause();
doAnswer((Answer<String>) invocation -> {
Object[] args = invocation.getArguments();
MockTarget mock1 = (MockTarget) invocation.getMock();
return "mock sayHello " + args[0];
}).when(mock).sayHello("doAnswer");
assertEquals(mock.sayHello("doAnswer"), "mock sayHello doAnswer");
// 1.doNothing, 2. throw RuntimeException
doNothing().doThrow(RuntimeException.class).when(mock).soSth();
mock.soSth();
Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy(mock::soSth);
}
verify
@Test
public void testVerifyInteractions() {
// mock creation
List mockedList = mock(List.class);
mockedList.clear();
// only clear() invoked
verify(mockedList, only()).clear();
verifyNoMoreInteractions(mockedList);
// 此处不会抛异常,因为是mock的list对象,非实际list对象
when(mockedList.get(1)).thenReturn("two");
assertEquals(mockedList.get(1), "two");
// using mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one");
// selective, explicit, highly readable verification
verify(mockedList).add("one");
verify(mockedList, times(1)).clear();
verify(mockedList, atLeastOnce()).add("one");
verify(mockedList, atMostOnce()).add("one");
verify(mockedList, atMost(1)).add("one");
verify(mockedList, atLeast(1)).add("one");
verify(mockedList, never()).add("never");
}
verify 之 after 与 timeout
after 会阻塞等满时间之后再往下执行,是固定等待多长时间的语义 timeout 在等待期内,拿到结果后立即向下执行,不做多余等待;是最多等待多长时间的语义
@Test
public void testAfterAndTimeout() throws Exception {
MockTarget mock = mockTarget;
doCallRealMethod().when(mock).callMethodWait(anyLong());
final long timeout = 500L;
final long delta = 100L;
// 异步调用
CompletableFuture<Void> async = CompletableFuture.runAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(timeout);
} catch (InterruptedException ignored) {
}
mock.sayHello();
mock.callMethod("test");
mock.callMethod("test");
});
// timeout() exits immediately with success when verification passes
// verify(mock, description("invoke not yet, This will print on failure")).callMethod("test");
verify(mock, timeout(timeout + delta).times(2)).callMethod("test");
// immediately success
verify(mock, timeout(10)).sayHello();
async.get();
// after() awaits full duration to check if verification passes
verify(mock, after(10).times(2)).callMethod("test");
verify(mock, after(10)).sayHello();
}
spy
句式 doXxx..when 当同一目标方法上定义了多个 mock 行为,后序 mock 可以覆盖前序 mock clearInvocations 仅清理之前的调用 reset 会重置为初始状态(所有中途的赋值都会被清理掉)
@Test
public void testDoReturn() {
// real creation
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
int mockSize = 100;
when(spy.size()).thenReturn(mockSize);
//size() method was stubbed - 100 is printed
assertEquals(spy.size(), mockSize);
// Overriding a previous exception-stubbing:
when(spy.size()).thenThrow(new IllegalStateException("not init"));
doReturn(mockSize).when(spy).size();
assertEquals(spy.size(), mockSize);
//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
Assertions.assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> spy.get(0));
doReturn("mock data").when(spy).get(1);
//using the spy calls real methods
spy.add("one");
assertEquals(spy.get(0), "one");
/*
Use this method in order to only clear invocations, when stubbing is non-trivial. Use-cases can be:
You are using a dependency injection framework to inject your mocks.
The mock is used in a stateful scenario. For example a class is Singleton which depends on your mock.
Try to avoid this method at all costs. Only clear invocations if you are unable to efficiently test your program.
*/
clearInvocations(spy);
verify(spy, times(0)).add("two");
reset(spy);
when(spy.size()).thenReturn(0);
assertEquals(spy.size(), 0);
}
▐ PowerMock
public enum TypeEnum {
Y("TRUE"),
N("FALSE");
private final String title;
TypeEnum(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
public final class FinalTarget {
public FinalTarget() { }
public final String finalMethod() {
return "Hello final!";
}
}
public class StaticTarget {
public static String firstMethod(String name) {
return "Hello " + name + " !";
}
public static String secondMethod() {
return "Hello no one!";
}
}
public class PartialTarget {
private String arg;
public PartialTarget(String arg) {
this.arg = arg;
}
public PartialTarget() { }
public String getArg() {
return arg;
}
private String privateWithArg(String arg) {
return "Hello privateWithArg! " + arg;
}
public String privateMethodCaller(String arg) {
return privateWithArg(arg) + " privateMethodCall.";
}
}
类注解
@RunWith(PowerMockRunner.class)
@PrepareForTest({StaticTarget.class, PartialTarget.class, TypeEnum.class, FinalTarget.class})
PowerMockito.mockStatic(StaticTarget.class);
StaticTarget.firstMethod("xxx");
PowerMockito.verifyStatic(StaticTarget.class); // 1
StaticTarget.firstMethod(invokeParam); // 2
PowerMockito.verifyStatic(StaticTarget.class, times(1));
PowerMockito.verifyStatic(StaticTarget.class, Mockito.atLeastOnce());
private
PartialTarget partialMock = PowerMockito.mock(PartialTarget.class);
doCallRealMethod().when(partialMock).privateMethodCaller(anyString());
PowerMockito.doReturn("mockResult").when(partialMock, "privateWithArg", any());
// *privateMethodCaller* will invoke method *privateWithArg*
String result = partialMock.privateMethodCaller("arg");
Assert.assertEquals(result, "mockResult privateMethodCall.");
PowerMockito.verifyPrivate(partialMock, times(1)).invoke("privateWithArg", "arg");
FinalTarget finalTarget = PowerMockito.mock(FinalTarget.class);
String finalReturn = "finalReturn";
PowerMockito.when(finalTarget.finalMethod()).thenReturn(finalReturn);
Assert.assertThat(finalTarget.finalMethod(), is(finalReturn));
String mockValue = "mock title";
TypeEnum typeMock = PowerMockito.mock(TypeEnum.class);
Whitebox.setInternalState(TypeEnum.class, "N", typeMock);
when(typeMock.getTitle()).thenReturn(mockValue);
Assert.assertEquals(TypeEnum.N.getTitle(), mockValue);
Assert.assertEquals(TypeEnum.Y.getTitle(), "TRUE");
String arg = "special arg";
PartialTarget partialWithArgSpy = PowerMockito.spy(new PartialTarget(arg));
whenNew(PartialTarget.class).withNoArguments().thenReturn(partialWithArgSpy);
PartialTarget partialNoArg = new PartialTarget();
Assert.assertEquals(partialNoArg.getArg(), arg);
verifyNew(PartialTarget.class).withNoArguments();
import org.assertj.core.api.Assertions;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import static org.hamcrest.core.Is.is;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.powermock.api.mockito.PowerMockito.doCallRealMethod;
import static org.powermock.api.mockito.PowerMockito.verifyNew;
import static org.powermock.api.mockito.PowerMockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;
@RunWith(PowerMockRunner.class)
@PrepareForTest({StaticTarget.class, PartialTarget.class, TypeEnum.class, FinalTarget.class})
public class PowerMockTest {
@Test
public void testStatic() throws Exception {
PowerMockito.mockStatic(StaticTarget.class);
String mockResult = "Static mock";
PowerMockito.when(StaticTarget.firstMethod(anyString())).thenReturn(mockResult);
String invokeParam = "any String parameter";
Assert.assertEquals(StaticTarget.firstMethod(invokeParam), mockResult);
// Verification of a static method is done in two steps.
PowerMockito.verifyStatic(StaticTarget.class); // 1
// StaticTarget.secondMethod();// not invoked
StaticTarget.firstMethod(invokeParam);// 2
// use argument matchers
PowerMockito.verifyStatic(StaticTarget.class); // 1
StaticTarget.firstMethod(anyString()); // 2
// atLeastOnce
PowerMockito.verifyStatic(StaticTarget.class, Mockito.atLeastOnce()); // 1
StaticTarget.firstMethod(anyString()); // 2
// times
PowerMockito.verifyStatic(StaticTarget.class, times(1)); // 1
StaticTarget.firstMethod(anyString()); // 2
// partial mocking of a private method & verifyPrivate
// PartialTarget partialNoArgSpy = PowerMockito.spy(new PartialTarget());
PartialTarget partialMock = PowerMockito.mock(PartialTarget.class);
doCallRealMethod().when(partialMock, "privateMethodCaller", anyString());
PowerMockito.doReturn("mockResult").when(partialMock, "privateWithArg", any());
// *privateMethodCaller* will invoke method *privateWithArg*
String result = partialMock.privateMethodCaller("arg");
Assert.assertEquals(result, "mockResult privateMethodCall.");
PowerMockito.verifyPrivate(partialMock, times(1)).invoke("privateWithArg", "arg");
// Final
FinalTarget finalTarget = PowerMockito.mock(FinalTarget.class);
String finalReturn = "finalReturn";
PowerMockito.when(finalTarget.finalMethod()).thenReturn(finalReturn);
Assert.assertThat(finalTarget.finalMethod(), is(finalReturn));
// enum
String mockValue = "mock title";
TypeEnum typeMock = PowerMockito.mock(TypeEnum.class);
Whitebox.setInternalState(TypeEnum.class, "N", typeMock);
when(typeMock.getTitle()).thenReturn(mockValue);
Assert.assertEquals(TypeEnum.N.getTitle(), mockValue);
Assert.assertEquals(TypeEnum.Y.getTitle(), "TRUE");
// verify New
String arg = "special arg";
PartialTarget partialWithArgSpy = PowerMockito.spy(new PartialTarget(arg));
whenNew(PartialTarget.class).withNoArguments().thenReturn(partialWithArgSpy);
PartialTarget partialNoArg = new PartialTarget();
Assert.assertEquals(partialNoArg.getArg(), arg);
verifyNew(PartialTarget.class).withNoArguments();
// throw exception
PowerMockito.doThrow(new ArrayStoreException("Mock secondMethod error")).when(StaticTarget.class);
StaticTarget.secondMethod();
// AssertJ: Exception assertions
Assertions.assertThatThrownBy(StaticTarget::secondMethod)
.isInstanceOf(ArrayStoreException.class)
.hasNoCause()
.hasMessage("Mock secondMethod error");
}
}
▐ AssertJ
isIn,isNotIn 和 matches 用于断言匹配条件 filteredOn 可以针对 assertThat 中传入的参数进行过滤,类似 java8 中Stream() 的 filter 方法 extracting 可以针对 assertThat 中传入的元组进行字段提取校验 assertThatExceptionOfType 和 assertThatThrownBy 可用于捕获预期的异常
// AssertJ provides wrappers for common exception types
Assertions.assertThatNoException();
Assertions.assertThatIOException();
Assertions.assertThatNullPointerException();
Assertions.assertThatIllegalStateException();
Assertions.assertThatIllegalArgumentException();
import org.assertj.core.api.Assertions;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.tuple;
public class AssertTest {
@Test
public void testAssertJ() {
String title = "foo";
AssertTarget assertTarget = new AssertTarget(title, 12, TypeEnum.Y);
String msg = "Illegal Argument error";
Exception cause = new NullPointerException("cause exception msg");
Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> assertTarget.throwIllegalArgumentException(msg, cause))
.withMessage(msg)
.withMessageContaining("Argument error")
.overridingErrorMessage("new error message")
.withCause(cause);
Assertions.assertThatThrownBy(() -> assertTarget.throwIllegalArgumentException(msg, cause))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Argument error");
Assertions.assertThat(assertTarget.getTitle())
// as() is used to describe the test and will be shown before the error message
.as("PartialTarget's arg is not match", assertTarget.getTitle())
.startsWith(title)
.endsWith(title)
.contains(title)
.isNotEqualTo("foo bar")
.isEqualToIgnoringCase("FOO")
.isEqualTo(title);
AssertTarget target1 = new AssertTarget("testTitle", 12, TypeEnum.N);
AssertTarget target2 = new AssertTarget("titleVal1", 16, TypeEnum.N);
AssertTarget target3 = new AssertTarget("titleVal2", 18, TypeEnum.Y);
AssertTarget target4 = new AssertTarget("titleVal3", 20, TypeEnum.N);
List<AssertTarget> assertTargetRing = Arrays.asList(target1, target2, target3);
Assertions.assertThat(target1.getNum()).withFailMessage("the num not matches").isEqualTo(12);
Assertions.assertThat(target1.getType().equals(TypeEnum.N)).isTrue();
Assertions.assertThat(target1).isIn(assertTargetRing);
Assertions.assertThat(target4).isNotIn(assertTargetRing);
Assertions.assertThat(target4).matches(e -> e.getNum() > 18 && e.getType().equals(TypeEnum.N));
Assertions.assertThat(assertTargetRing)
// extracting multiple values at once grouped in tuples
.extracting("num", "type.title")
.contains(tuple(16, TypeEnum.N.getTitle())
, tuple(18, TypeEnum.Y.getTitle()));
Assertions.assertThat(assertTargetRing)
// filtering a collection before asserting
.filteredOn(e -> e.getTitle().startsWith("title"))
.extracting(AssertTarget::getNum)
.contains(16, 18);
}
}
真香
利用 Mockiton 做常规类和接口的 mock
PowerMock 则可以 mock 静态方法,私有方法,final 方法,枚举,构造函数等
AssertJ 流式风格,增强 assert 判断逻辑和校验异常流程
写在最后
我们HC不限,可以直接跟老板聊,招聘流程快。期待你的加入,共建To B端最具代表性的商业技术体系,迎接产业互联网的到来。地点杭州阿里巴巴西溪园区。欢迎各路大侠加入!
简历投递邮箱📮:xzc270316@alibaba-inc.com
Mockito: https://site.mockito.org
AssertJ: https://assertj.github.io/doc