implementation parameter verification

This commit is contained in:
金戟 2020-10-31 21:02:35 +08:00
parent 4b1b1c3126
commit d816726f4f
16 changed files with 233 additions and 90 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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";
}

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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);
List<Object[]> records = getInvokeRecord(mockMethodName, testCaseName);
if (isConstructor) {
records.add(args);
} else {
records.add(slice(args, 1));
}
/**
* Record mock method invoke event
*/
public static void recordMockInvoke(Object[] args) {
countMockInvoke();
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;
}
}

View File

@ -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]);
}
}