mirror of
https://github.com/alibaba/testable-mock.git
synced 2025-02-13 13:20:32 +08:00
implementation parameter verification
This commit is contained in:
parent
4b1b1c3126
commit
d816726f4f
@ -63,13 +63,13 @@ class DemoMockServiceTest {
|
||||
@Test
|
||||
void should_able_to_mock_new_object() throws Exception {
|
||||
assertEquals("mock_something", demoService.newFunc());
|
||||
verify("createBlackBox").times(1);
|
||||
verify("createBlackBox").with("something");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_able_to_mock_member_method() throws Exception {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"));
|
||||
verify("innerFunc").times(1);
|
||||
verify("innerFunc").with("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -59,13 +59,13 @@ internal class DemoMockServiceTest {
|
||||
@Test
|
||||
fun should_able_to_mock_new_object() {
|
||||
assertEquals("mock_something", demoService.newFunc())
|
||||
verify("createBlackBox").times(1)
|
||||
verify("createBlackBox").with("something")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun should_able_to_mock_member_method() {
|
||||
assertEquals("{ \"res\": \"mock_hello\"}", demoService.outerFunc("hello"))
|
||||
verify("innerFunc").times(1)
|
||||
verify("innerFunc").with("hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -3,7 +3,6 @@ package com.alibaba.testable.demo.util
|
||||
import org.junit.jupiter.api.Test
|
||||
import com.alibaba.testable.core.annotation.TestableMock
|
||||
import com.alibaba.testable.core.tool.TestableTool.verify
|
||||
import com.alibaba.testable.demo.util.PathUtil
|
||||
import java.io.File
|
||||
|
||||
class PathUtilTest {
|
||||
|
@ -12,5 +12,7 @@ public class ConstPool {
|
||||
public static final String TEST_POSTFIX = "Test";
|
||||
public static final String TESTABLE_INJECT_REF = "_testableInternalRef";
|
||||
|
||||
public static final String FIELD_TARGET_METHOD = "targetMethod";
|
||||
|
||||
public static final String TESTABLE_MOCK = "com.alibaba.testable.core.annotation.TestableMock";
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package com.alibaba.testable.agent.handler;
|
||||
|
||||
import com.alibaba.testable.agent.constant.ConstPool;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import org.objectweb.asm.tree.*;
|
||||
|
||||
@ -24,7 +25,7 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
private static final String METHOD_CURRENT_SOURCE_METHOD_NAME = "currentSourceMethodName";
|
||||
private static final String METHOD_RECORD_MOCK_INVOKE = "recordMockInvoke";
|
||||
private static final String SIGNATURE_TESTABLE_UTIL_METHOD = "(Ljava/lang/Object;)Ljava/lang/String;";
|
||||
private static final String SIGNATURE_INVOKE_COUNTER_METHOD = "()V";
|
||||
private static final String SIGNATURE_INVOKE_RECORDER_METHOD = "([Ljava/lang/Object;Z)V";
|
||||
private static final Map<String, String> FIELD_TO_METHOD_MAPPING = new HashMap<String, String>() {{
|
||||
put(FIELD_TEST_CASE, METHOD_CURRENT_TEST_CASE_NAME);
|
||||
put(FIELD_SOURCE_METHOD, METHOD_CURRENT_SOURCE_METHOD_NAME);
|
||||
@ -104,6 +105,7 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
il.add(getIntInsn(size));
|
||||
il.add(new TypeInsnNode(ANEWARRAY, ClassUtil.CLASS_OBJECT));
|
||||
for (int i = 0; i < size; i++) {
|
||||
mn.maxStack += 2;
|
||||
il.add(new InsnNode(DUP));
|
||||
il.add(getIntInsn(i));
|
||||
ImmutablePair<Integer, Integer> code = getLoadParameterByteCode(types.get(i));
|
||||
@ -115,11 +117,26 @@ public class TestClassHandler extends BaseClassHandler {
|
||||
}
|
||||
il.add(new InsnNode(AASTORE));
|
||||
}
|
||||
if (isMockForConstructor(mn)) {
|
||||
il.add(new InsnNode(ICONST_1));
|
||||
} else {
|
||||
il.add(new InsnNode(ICONST_0));
|
||||
}
|
||||
il.add(new MethodInsnNode(INVOKESTATIC, CLASS_INVOKE_RECORD_UTIL, METHOD_RECORD_MOCK_INVOKE,
|
||||
SIGNATURE_INVOKE_COUNTER_METHOD, false));
|
||||
SIGNATURE_INVOKE_RECORDER_METHOD, false));
|
||||
mn.instructions.insertBefore(mn.instructions.get(0), il);
|
||||
}
|
||||
|
||||
private boolean isMockForConstructor(MethodNode mn) {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
String method = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD, null);
|
||||
if (ConstPool.CONSTRUCTOR.equals(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ImmutablePair<Integer, Integer> getLoadParameterByteCode(Byte type) {
|
||||
switch (type) {
|
||||
case ClassUtil.TYPE_BYTE:
|
||||
|
@ -6,6 +6,7 @@ import com.alibaba.testable.agent.handler.TestClassHandler;
|
||||
import com.alibaba.testable.agent.tool.ImmutablePair;
|
||||
import com.alibaba.testable.agent.model.MethodInfo;
|
||||
import com.alibaba.testable.agent.tool.ComparableWeakRef;
|
||||
import com.alibaba.testable.agent.util.AnnotationUtil;
|
||||
import com.alibaba.testable.agent.util.ClassUtil;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
@ -28,7 +29,6 @@ import static com.alibaba.testable.agent.util.ClassUtil.toDotSeparateFullClassNa
|
||||
public class TestableClassTransformer implements ClassFileTransformer {
|
||||
|
||||
private final Set<ComparableWeakRef<String>> loadedClassNames = ComparableWeakRef.getWeekHashSet();
|
||||
private static final String TARGET_METHOD = "targetMethod";
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
|
||||
@ -90,7 +90,7 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
for (AnnotationNode an : mn.visibleAnnotations) {
|
||||
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_MOCK)) {
|
||||
String targetClass = ClassUtil.toSlashSeparateFullClassName(methodDescPair.left);
|
||||
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
|
||||
String targetMethod = AnnotationUtil.getAnnotationParameter(an, ConstPool.FIELD_TARGET_METHOD, mn.name);
|
||||
if (targetMethod.equals(ConstPool.CONSTRUCTOR)) {
|
||||
String sourceClassName = ClassUtil.getSourceClassName(cn.name);
|
||||
methodInfos.add(new MethodInfo(sourceClassName, targetMethod, mn.name, mn.desc));
|
||||
@ -112,18 +112,4 @@ public class TestableClassTransformer implements ClassFileTransformer {
|
||||
return pos < 0 ? null : ImmutablePair.of(desc.substring(1, pos + 1), "(" + desc.substring(pos + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read value of annotation parameter
|
||||
*/
|
||||
private <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) {
|
||||
if (an.values != null) {
|
||||
for (int i = 0; i < an.values.size(); i += 2) {
|
||||
if (an.values.get(i).equals(key)) {
|
||||
return (T)(an.values.get(i + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class AnnotationUtil {
|
||||
|
||||
/**
|
||||
* Read value of annotation parameter
|
||||
*/
|
||||
public static <T> T getAnnotationParameter(AnnotationNode an, String key, T defaultValue) {
|
||||
if (an.values != null) {
|
||||
for (int i = 0; i < an.values.size(); i += 2) {
|
||||
if (an.values.get(i).equals(key)) {
|
||||
return (T)(an.values.get(i + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package com.alibaba.testable.agent.transformer;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
import static com.alibaba.testable.agent.util.CollectionUtil.listOf;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TestableClassTransformerTest {
|
||||
|
||||
private TestableClassTransformer transformer = new TestableClassTransformer();
|
||||
|
||||
@Test
|
||||
void should_get_annotation_parameter() {
|
||||
AnnotationNode an = new AnnotationNode("");
|
||||
an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue");
|
||||
assertEquals("testValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testKey", "none"));
|
||||
assertEquals("demoValue", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "demoKey", "none"));
|
||||
assertEquals("none", PrivateAccessor.invoke(transformer, "getAnnotationParameter", an, "testValue", "none"));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.alibaba.testable.agent.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
|
||||
import static com.alibaba.testable.agent.util.CollectionUtil.listOf;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AnnotationUtilTest {
|
||||
|
||||
@Test
|
||||
void should_get_annotation_parameter() {
|
||||
AnnotationNode an = new AnnotationNode("");
|
||||
an.values = listOf((Object)"testKey", "testValue", "demoKey", "demoValue");
|
||||
assertEquals("testValue", AnnotationUtil.getAnnotationParameter(an, "testKey", "none"));
|
||||
assertEquals("demoValue", AnnotationUtil.getAnnotationParameter(an, "demoKey", "none"));
|
||||
assertEquals("none", AnnotationUtil.getAnnotationParameter(an, "testValue", "none"));
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package com.alibaba.testable.core.accessor;
|
||||
|
||||
import com.alibaba.testable.core.util.InvokeRecordUtil;
|
||||
import com.alibaba.testable.core.util.TypeUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@ -46,4 +47,18 @@ public class PrivateAccessor {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static <T> T invokeStatic(Class<?> clazz, String method, Object... args) {
|
||||
try {
|
||||
Class<?>[] cls = TypeUtil.getClassesFromObjects(args);
|
||||
Method declaredMethod = TypeUtil.getMethodByNameAndParameterTypes(clazz.getDeclaredMethods(), method, cls);
|
||||
if (declaredMethod != null) {
|
||||
declaredMethod.setAccessible(true);
|
||||
return (T)declaredMethod.invoke(null, args);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,24 @@ package com.alibaba.testable.core.error;
|
||||
*/
|
||||
public class VerifyFailedError extends AssertionError {
|
||||
|
||||
public VerifyFailedError(int actualCount, int expectedCount) {
|
||||
super(getErrorMessage(actualCount, expectedCount));
|
||||
public VerifyFailedError(String message) {
|
||||
super(getErrorMessage(message));
|
||||
}
|
||||
|
||||
private static String getErrorMessage(int actualCount, int expectedCount) {
|
||||
return "\nExpected times : " + expectedCount + "\nActual times : " + actualCount;
|
||||
public VerifyFailedError(String expected, String actual) {
|
||||
super(getErrorMessage(expected, actual));
|
||||
}
|
||||
|
||||
public VerifyFailedError(String message, String expected, String actual) {
|
||||
super(getErrorMessage(message) + getErrorMessage(expected, actual));
|
||||
}
|
||||
|
||||
private static String getErrorMessage(String message) {
|
||||
return "\n" + message.substring(0, 1).toUpperCase() + message.substring(1);
|
||||
}
|
||||
|
||||
private static String getErrorMessage(String expected, String actual) {
|
||||
return "\nExpected " + expected + "\n Actual " + actual;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class InvokeCounter {
|
||||
|
||||
private final int actualCount;
|
||||
|
||||
public InvokeCounter(int actualCount) {
|
||||
this.actualCount = actualCount;
|
||||
}
|
||||
|
||||
public void times(int expectedCount) {
|
||||
if (expectedCount != actualCount) {
|
||||
throw new VerifyFailedError(actualCount, expectedCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package com.alibaba.testable.core.tool;
|
||||
|
||||
import com.alibaba.testable.core.error.VerifyFailedError;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author flin
|
||||
*/
|
||||
public class InvokeVerifier {
|
||||
|
||||
private final List<Object[]> records;
|
||||
|
||||
public InvokeVerifier(List<Object[]> records) {
|
||||
this.records = records;
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1) {
|
||||
with(new Object[]{arg1});
|
||||
return this;
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2) {
|
||||
with(new Object[]{arg1, arg2});
|
||||
return this;
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2, Object arg3) {
|
||||
with(new Object[]{arg1, arg2, arg3});
|
||||
return this;
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4) {
|
||||
with(new Object[]{arg1, arg2, arg3, arg4});
|
||||
return this;
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) {
|
||||
with(new Object[]{arg1, arg2, arg3, arg4, arg5});
|
||||
return this;
|
||||
}
|
||||
|
||||
public InvokeVerifier with(Object[] args) {
|
||||
if (records.isEmpty()) {
|
||||
throw new VerifyFailedError("has not more invoke");
|
||||
}
|
||||
Object[] record = records.get(0);
|
||||
if (record.length != args.length) {
|
||||
throw new VerifyFailedError(desc(args), desc(record));
|
||||
}
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (!args[i].getClass().equals(record[i].getClass())) {
|
||||
throw new VerifyFailedError("parameter " + (i + 1) + " type mismatch",
|
||||
": " + args[i].getClass(), ": " + record[i].getClass());
|
||||
}
|
||||
if (!args[i].equals(record[i])) {
|
||||
throw new VerifyFailedError("parameter " + (i + 1) + " mismatched", desc(args), desc(record));
|
||||
}
|
||||
}
|
||||
records.remove(0);
|
||||
return this;
|
||||
}
|
||||
|
||||
private String desc(Object[] args) {
|
||||
StringBuilder sb = new StringBuilder(": ");
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(args[i]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public InvokeVerifier times(int expectedCount) {
|
||||
if (expectedCount != records.size()) {
|
||||
throw new VerifyFailedError("times: " + records.size(), "times: " + expectedCount);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
@ -27,10 +27,10 @@ public class TestableTool {
|
||||
* Get counter to check whether specified mock method invoked
|
||||
* @param mockMethodName name of a mock method
|
||||
*/
|
||||
public static InvokeCounter verify(String mockMethodName) {
|
||||
public static InvokeVerifier verify(String mockMethodName) {
|
||||
String testClass = Thread.currentThread().getStackTrace()[InvokeRecordUtil.INDEX_OF_TEST_CLASS].getClassName();
|
||||
String testCaseName = TestableUtil.currentTestCaseName(testClass);
|
||||
return new InvokeCounter(InvokeRecordUtil.getInvokeCount(mockMethodName, testCaseName));
|
||||
return new InvokeVerifier(InvokeRecordUtil.getInvokeRecord(mockMethodName, testCaseName));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -8,8 +10,11 @@ import java.util.Map;
|
||||
*/
|
||||
public class InvokeRecordUtil {
|
||||
|
||||
private static final Map<String, Integer> INVOKE_RECORDS = new HashMap<String, Integer>();
|
||||
private final static String JOINER = "->";
|
||||
/**
|
||||
* Mock method name -> List of invoke parameters
|
||||
*/
|
||||
private static final Map<String, List<Object[]>> INVOKE_RECORDS = new HashMap<String, List<Object[]>>();
|
||||
private final static String JOINER = "::";
|
||||
|
||||
/**
|
||||
* [0]Thread -> [1]TestableUtil/TestableTool -> [2]TestClass
|
||||
@ -17,32 +22,40 @@ public class InvokeRecordUtil {
|
||||
public static final int INDEX_OF_TEST_CLASS = 2;
|
||||
|
||||
/**
|
||||
* Record mock method invoke count
|
||||
* Record mock method invoke event
|
||||
*/
|
||||
public static void countMockInvoke() {
|
||||
public static void recordMockInvoke(Object[] args, boolean isConstructor) {
|
||||
StackTraceElement mockMethodTraceElement = Thread.currentThread().getStackTrace()[INDEX_OF_TEST_CLASS];
|
||||
String mockMethodName = mockMethodTraceElement.getMethodName();
|
||||
String testClass = mockMethodTraceElement.getClassName();
|
||||
String testCaseName = TestableUtil.currentTestCaseName(testClass);
|
||||
String key = testCaseName + JOINER + mockMethodName;
|
||||
int count = getInvokeCount(mockMethodName, testCaseName);
|
||||
INVOKE_RECORDS.put(key, count + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record mock method invoke event
|
||||
*/
|
||||
public static void recordMockInvoke(Object[] args) {
|
||||
countMockInvoke();
|
||||
List<Object[]> records = getInvokeRecord(mockMethodName, testCaseName);
|
||||
if (isConstructor) {
|
||||
records.add(args);
|
||||
} else {
|
||||
records.add(slice(args, 1));
|
||||
}
|
||||
INVOKE_RECORDS.put(key, records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mock method invoke count
|
||||
*/
|
||||
public static int getInvokeCount(String mockMethodName, String testCaseName) {
|
||||
public static List<Object[]> getInvokeRecord(String mockMethodName, String testCaseName) {
|
||||
String key = testCaseName + JOINER + mockMethodName;
|
||||
Integer count = INVOKE_RECORDS.get(key);
|
||||
return (count == null) ? 0 : count;
|
||||
List<Object[]> records = INVOKE_RECORDS.get(key);
|
||||
return (records == null) ? new LinkedList<Object[]>() : records;
|
||||
}
|
||||
|
||||
private static Object[] slice(Object[] args, int firstIndex) {
|
||||
int size = args.length - firstIndex;
|
||||
if (size <= 0) {
|
||||
return new Object[0];
|
||||
}
|
||||
Object[] slicedArgs = new Object[size];
|
||||
System.arraycopy(args, firstIndex, slicedArgs, 0, size);
|
||||
return slicedArgs;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package com.alibaba.testable.core.util;
|
||||
|
||||
import com.alibaba.testable.core.accessor.PrivateAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class InvokeRecordUtilTest {
|
||||
|
||||
@Test
|
||||
void should_slice_array() {
|
||||
Object[] args = new Object[]{"1", "2", "3"};
|
||||
Object[] slicedArgs = PrivateAccessor.invokeStatic(InvokeRecordUtil.class, "slice", args, 1);
|
||||
assertEquals(2, slicedArgs.length);
|
||||
assertEquals("2", slicedArgs[0]);
|
||||
assertEquals("3", slicedArgs[1]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user