Java 在一个会话中记录方法调用,以便在将来的测试会话中重播?
我有一个后端系统,我们使用第三方JavaAPI从我们自己的应用程序进行访问。我可以作为一个普通用户和其他用户一起访问系统,但我对它没有神圣的权力 因此,为了简化测试,我希望运行一个真正的会话并记录API调用,并将它们持久化(最好是作为可编辑代码),因此,我们可以稍后使用API调用进行干测试运行,只返回录制会话的相应响应——这是重要的部分——而无需与上述后端系统对话 因此,如果我的申请表中包含以下行:Java 在一个会话中记录方法调用,以便在将来的测试会话中重播?,java,Java,我有一个后端系统,我们使用第三方JavaAPI从我们自己的应用程序进行访问。我可以作为一个普通用户和其他用户一起访问系统,但我对它没有神圣的权力 因此,为了简化测试,我希望运行一个真正的会话并记录API调用,并将它们持久化(最好是作为可编辑代码),因此,我们可以稍后使用API调用进行干测试运行,只返回录制会话的相应响应——这是重要的部分——而无需与上述后端系统对话 因此,如果我的申请表中包含以下行: Object b = callBackend(a); 我希望框架首先捕获callBacken
Object b = callBackend(a);
我希望框架首先捕获callBackend()
在给定参数a的情况下返回b,然后在以后的任何时候进行试运行时说“嘿,给定a,此调用应返回b”。a和b的值将是相同的(如果不是,我们将重新运行录制步骤)
我可以重写提供API的类,以便捕获的所有方法调用都将通过我的代码进行(即,不需要字节码插装来改变我控制之外的类的行为)
要做到这一点,我应该研究什么样的框架
编辑:请注意赏金猎人应该提供实际的代码来演示我所寻找的行为 如果我正确理解了您的问题,您应该试试db4o
您将使用db4o存储对象,并在稍后恢复到mock和JUnit测试。几个月前,在规划大型应用程序的大规模技术重构时,我对非回归测试也有同样的需求,并且。。。我没有找到可用的框架 事实上,重放可能特别困难,并且可能只在特定的上下文中工作-没有(或很少)具有标准复杂性的应用程序可以真正被认为是无状态的。在使用关系数据库测试持久性代码时,这是一个常见问题。为了相关,必须恢复完整的系统初始状态,并且每个重播步骤必须以相同的方式影响全局状态。当一个系统状态被分为数据库、文件、内存等部分时,这就成为一个挑战。。。让我们猜测一下,如果某个地方使用了从系统时钟获取的时间戳,会发生什么 所以更实际的选择是只记录。。。然后为后续的运行做一个聪明的比较 根据您计划的运行次数,在应用程序上进行人工驱动的会话可能就足够了,或者您必须在机器人玩应用程序用户界面的自动化场景中进行投资 首先记录:您可以使用动态代理接口或方面编程来拦截方法调用,并捕获调用前后的状态。这可能意味着:转储相关的数据库表,复制一些文件,以XML等文本格式序列化Java对象 然后将此引用捕获与新运行进行比较。这种比较应该调整为从每个状态中排除任何不相关的元素,如行标识符、时间戳、文件名。。。仅比较后端增值显著的数据 最后,没有什么是真正标准的,通常只有几个特定的脚本和代码就足以实现目标:检测尽可能多的错误,并尝试防止非预期的副作用。
//pseudocode
// pseudocode
class LogMethod {
List<String> parameters;
String method;
addCallTo(String method, List<String> params):
this.method = method;
parameters = params;
}
}
类日志方法{
列出参数;
字符串方法;
addCallTo(字符串方法,列表参数):
这个方法=方法;
参数=参数;
}
}
在测试方法中的每次调用之前,都有一个
LogMethods
列表,并调用newlogmethod().addCallTo()
在此之前,我应该说,我同意伊夫·马丁回答中的一些担忧:这样一个系统可能会令人沮丧,最终没有乍一看的那么有用
也就是说,从技术角度来看,这是一个有趣的问题,我不能不去尝试。我以一种相当通用的方式记录方法调用。这里定义的CallLoggingProxy
类允许如下使用
Calendar original = CallLoggingProxy.create(Calendar.class, Calendar.getInstance());
original.getTimeInMillis(); // 1368311282470
CallLoggingProxy.ReplayInfo replayInfo = CallLoggingProxy.getReplayInfo(original);
// Persist the replay info to disk, serialize to a DB, whatever floats your boat.
// Come back and load it up later...
Calendar replay = CallLoggingProxy.replay(Calendar.class, replayInfo);
replay.getTimeInMillis(); // 1368311282470
您可以想象使用CallLoggingProxy包装您的API对象。在将其传递到测试方法之前创建,然后捕获数据,并使用您最喜欢的序列化系统将其持久化。稍后,当您想要运行测试时,可以加载数据备份,使用CallLoggingProxy.replay
基于数据创建新实例,并将其传递到方法中
CallLoggingProxy
是使用编写的,因为Java的原生Proxy
仅限于针对接口工作。这应该涵盖一般用例,但要记住一些限制:
- 此方法无法代理声明为
final
的类。(不易修复;这是一个系统限制)
- gist假设方法的相同输入总是产生相同的输出。(更容易修复;
ReplayInfo
需要跟踪每个输入的调用序列,而不是单个输入/输出对。)
- 要点甚至都不是线程安全的(相当容易修复;只需要一点思考和努力)
很明显,要点只是一个概念证明,所以它也没有经过非常彻底的测试,但我相信一般原则是正确的。也有可能有一个更全面的框架来实现这类目标,但如果确实存在这样的东西,我不知道
如果您决定继续使用replay方法,那么希望这将足以为您提供一个可能的工作方向。这可以通过面向方面编程的AOP来完成。它允许通过字节码操作拦截方法调用。搜索一些例子
在一种情况下,这可以进行录制,而在另一种情况下,这可以进行回放
public interface MyApi {
public String getMySpouse(String myName);
public int getMyAge(String myName);
...
}
public class RecordedInformation {
private String methodName;
private Object[] args;
private Object returnValue;
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
public Object getReturnValue() {
return returnType;
}
public void setReturnValue(Object returnValue) {
this.returnValue = returnValue;
}
@Override
public int hashCode() {
return super.hashCode(); //change your implementation as you like!
}
@Override
public boolean equals(Object obj) {
return super.equals(obj); //change your implementation as you like!
}
}
public class RecordReplyManager implements java.lang.reflect.InvocationHandler {
private Object objOfApi;
private boolean isForRecording;
public static Object newInstance(Object obj, boolean isForRecording) {
return java.lang.reflect.Proxy.newProxyInstance(
obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
new RecordReplyManager(obj, isForRecording));
}
private RecordReplyManager(Object obj, boolean isForRecording) {
this.objOfApi = obj;
this.isForRecording = isForRecording;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;
if (isForRecording) {
try {
System.out.println("recording...");
System.out.println("method name: " + method.getName());
System.out.print("method arguments:");
for (Object arg : args) {
System.out.print(" " + arg);
}
System.out.println();
result = method.invoke(objOfApi, args);
System.out.println("result: " + result);
RecordedInformation recordedInformation = new RecordedInformation();
recordedInformation.setMethodName(method.getName());
recordedInformation.setArgs(args);
recordedInformation.setReturnValue(result);
//persist your information
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (Exception e) {
throw new RuntimeException("unexpected invocation exception: " +
e.getMessage());
} finally {
// do nothing
}
return result;
} else {
try {
System.out.println("replying...");
System.out.println("method name: " + method.getName());
System.out.print("method arguments:");
for (Object arg : args) {
System.out.print(" " + arg);
}
RecordedInformation recordedInformation = new RecordedInformation();
recordedInformation.setMethodName(method.getName());
recordedInformation.setArgs(args);
//if your invocation information (this RecordedInformation) is found in the previously collected map, then return the returnValue from that RecordedInformation.
//if corresponding RecordedInformation does not exists then invoke the real method (like in recording step) and wrap the collected information into RecordedInformation and persist it as you like!
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (Exception e) {
throw new RuntimeException("unexpected invocation exception: " +
e.getMessage());
} finally {
// do nothing
}
return result;
}
}
}
MyApi realApi = new RealApi(); // using new or whatever way get your service implementation (API implementation)
MyApi myApiWithRecorder = (MyApi) RecordReplyManager.newInstance(realApi, true); // true for recording
myApiWithRecorder.getMySpouse("richard"); // to record getMySpouse
myApiWithRecorder.getMyAge("parker"); // to record getMyAge
...
MyApi realApi = new RealApi(); // using new or whatever way get your service implementation (API implementation)
MyApi myApiWithReplayer = (MyApi) RecordReplyManager.newInstance(realApi, false); // false for replaying
myApiWithReplayer.getMySpouse("richard"); // to replay getMySpouse
myApiWithRecorder.getMyAge("parker"); // to replay getMyAge
...
//You can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);
//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
//following prints "first"
System.out.println(mockedList.get(0));
//following throws runtime exception
System.out.println(mockedList.get(1));
//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
ResultObject b = callBackend(a);
...
ResultObject callBackend(SourceObject source) {
...
}
@Recorded
ResultObject callBackend(SourceObject source) {
...
}
void testCallBackend() {
//arrange
SourceObject sourceObject1 = new SourceObject();
sourceObject1.setState(...); // testrecorder can use setters but is not limited to them
... // setting up backend
... // setting up globals, mocking inputs
//act
ResultObject resultObject1 = backend.callBackend(sourceObject1);
//assert
assertThat(resultObject, new GenericMatcher() {
... // property matchers
}.matching(ResultObject.class));
... // assertions on backend and sourceObject1 for potential side effects
... // assertions on outputs and globals
}