在一个会话中logging方法调用以在将来的testing会话中重放?
我有一个后端系统,我们使用第三方的Java API来访问我们自己的应用程序。 我可以像其他用户一样以普通用户的身份访问系统,但我没有神圣的权力。
因此,为了简化testing,我想运行一个真正的会话并loggingAPI调用,并将它们保存起来(最好是可编辑的代码),以便稍后使用API调用进行干运行testing,然后从logging会话中返回相应的响应。这是重要的一部分 – 不需要跟上面提到的后端系统。
所以如果我的应用程序包含表单上的行:
Object b = callBackend(a);
我希望框架首先捕获callBackend()
返回给定的参数a,然后当我在稍后的干运行时说:“嘿,给这个电话应该返回b”。 a和b的值将是相同的(如果不是,我们将重新运行logging步骤)。
我可以重写提供API的类,以便捕获的所有方法调用将通过我的代码(即字节代码检测来改变我的控制之外的类的行为是没有必要的)。
我应该研究什么框架来做到这一点?
编辑:请注意,赏金猎人应提供实际的代码,展示我所寻找的行为。
其实你可以通过使用代理模式来构build这样的框架或模板。 在这里我解释一下,如何使用dynamic代理模式来实现。 这个想法是,
- 编写一个代理pipe理器来获取API的logging器和播放器代理服务器 !
- 编写一个包装类来存储收集到的信息,并实现
hashCode
和equals
包装类的方法,以便像Map
这样的数据结构进行高效的查找。 - 最后使用logging器代理为重放目的logging和重播代理。
录音机如何工作:
- 调用真实的API
- 收集调用信息
- 将数据保留在预期的持久性上下文中
代理人如何工作:
- 收集方法信息(方法名称,参数)
- 如果收集的信息与先前logging的信息匹配,则返回之前收集的返回值。
- 如果返回的值不匹配,则坚持收集的信息(如您所愿)。
现在,让我们看看实现。 如果你的API是MyApi
就像下面这样:
public interface MyApi { public String getMySpouse(String myName); public int getMyAge(String myName); ... }
现在我们将logging并重播public String getMySpouse(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! } }
现在主要部分是RecordReplyManager
。 这个RecordReplyManager
为你提供API的代理对象 ,取决于你的录音或者重放的需要。
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; } } }
如果你想logging方法调用,你只需要得到一个如下所示的API代理:
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 ...
你做完了!
编辑:logging器和重放器的基本步骤可以用上面提到的方法完成。 现在它取决于你,你要如何使用或执行这些步骤。 你可以做任何你想要的,无论你喜欢在录音机和代码块,只select你的实现!
我应该在这里加上前面的话来说,我在伊夫·马丁的回答中分享了一些担忧:这样的一个系统可能会令人沮丧,而且最终会比起初看起来没那么有用。
这就是说,从技术的angular度来看,这是一个有趣的问题,我不能不去看看。 我把一个要点以一种相当一般的方式logging方法调用。 在CallLoggingProxy
定义的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.create
传递给您的API对象之前,将其传递到您的testing方法中,然后捕获数据,并使用您最喜欢的序列化系统将其持久化。 稍后,当您想要运行testing时,可以加载数据,使用CallLoggingProxy.replay
基于数据创build新实例,然后将其传递到您的方法中。
CallLoggingProxy
是使用Javassist编写的,因为Java的本地Proxy
仅限于处理接口。 这应该涵盖一般的用例,但有一些限制需要记住:
- 声明为
final
类不能用此方法代理。 (不容易修复;这是一个系统限制) - 要点假设相同的input到一个方法将始终产生相同的输出。 (更容易
ReplayInfo
;ReplayInfo
将需要跟踪每个input的调用序列,而不是单个input/输出对。) - 要点甚至不是线程安全的(相当容易修复;只需要一点思考和努力)
这个要点显然只是一个概念的certificate,所以也没有经过彻底的testing,但我相信总的原则是正确的。 也有可能有一个更完善的框架来实现这样的目标,但如果这样的事情确实存在,我不知道。
如果你决定继续重播,那么希望这足以让你有一个可能的工作方向。
我在几个月前有同样的需求进行非回归testing,当规划一个大型应用程序的重大技术重构和…我没有发现任何可用的框架 。
实际上,重放可能特别困难,只能在特定的环境下工作 – 没有(或很less)具有标准复杂性的应用程序可以被认为是无状态的。 使用关系数据库testing持久性代码时,这是一个常见的问题。 为了相关,必须恢复完整的系统初始状态,并且每个重放步骤必须以同样的方式影响全局状态。 当一个系统状态被分解成像数据库,文件,内存等块时,这成为了一个挑战。让我们猜测如果从某个系统时钟获取的时间戳被使用,会发生什么?
所以更实用的select是只logging…然后做一个聪明的比较后续运行。
根据您计划的运行次数,应用程序上的人为驱动会话可能已经足够,或者您必须在使用应用程序用户界面的机器人中投资自动化场景。
首先要logging:可以使用dynamic代理接口或方面编程拦截方法调用,并在调用之前和之后捕获状态。 这可能意味着:转储相关的数据库表,复制一些文件,以XML等文本格式序列化Java对象。
然后比较这个参考捕获与新的运行。 应该调整这种比较,以排除每个状态的任何不相关的元素,如行标识符,时间戳,文件名…只比较后端的增值闪耀的数据。
最后没有什么是真正的标准,通常一些特定的脚本和代码可能足以实现这一目标:尽可能多地检测错误,并设法防止不期望的副作用。
这可以通过AOP ,面向方面的编程来完成。 它允许通过字节代码操作来拦截方法调用。 做一些search的例子。
在其中一种情况下,可以在其他回放中进行录制。
指针: 维基百科 ,AspectJ,Spring AOP。
不幸的是,有一点在java语法之外有所改变,一个简单的例子可以在其他地方find。 解释。
也许结合unit testing/一些嘲笑testing框架离线testing与logging的数据。
你可以看看“Mockito”
例:
//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));
之后你可以重播每个testing多次,它会返回你input的数据。
// pseudocode class LogMethod { List<String> parameters; String method; addCallTo(String method, List<String> params): this.method = method; parameters = params; } }
在您的testing方法的每次调用之前,都有一个new LogMethod().addCallTo()
列表,并调用new LogMethod().addCallTo()
。
回放API调用的想法听起来像事件源模式的用例。 Martin Fowler在这里有一篇很好的文章。 这是一个很好的模式,将事件作为一系列对象进行logging,然后根据需要重放事件序列。
这个模式的实现使用Akka所谓的Eventsourced ,它可以帮助你build立你所需要的系统types。
几年前我也有类似的问题。 上述解决scheme都不适用于不是纯function的方法(无副作用)。 主要任务是在我看来:
- 如何提取logging对象的快照(不仅限于实现
Serializable
对象) - 如何以可读的方式生成序列化表示的testing代码(不仅限于bean,原语和集合)
所以我必须用我自己的方式 – 用testrecorder 。
例如,给出:
ResultObject b = callBackend(a); ... ResultObject callBackend(SourceObject source) { ... }
你将只需要注解这样的方法:
@Recorded ResultObject callBackend(SourceObject source) { ... }
并使用testrecorder代理启动您的应用程序(应该logging的应用程序)。 Testrecorder将为您pipe理所有任务,例如:
- 序列化参数,结果,状态,exception(完整的对象图!)
- 为对象构造和对象匹配find可读的表示
- 从序列化数据生成一个testing
- 您可以将logging扩展到全局variables,input和输出注释
testing的例子如下所示:
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 }
如果我正确地理解了你的问题,你应该尝试db4o。
您将使用db4o存储对象,稍后再恢复为模拟和JUnittesting。