40

我有一个后端系统,我们使用第三方 Java API 从我们自己的应用程序访问。我可以与其他用户一起以普通用户的身份访问系统,但我对它没有神圣的权力。

因此,为了简化测试,我想运行一个真实的会话并记录 API 调用,并将它们持久化(最好作为可编辑代码),这样我们就可以稍后使用 API 调用进行干测试运行,只需从记录会话中返回相应的响应 - 和这是重要的部分 - 无需与上述后端系统交谈。

因此,如果我的应用程序包含表单上的行:

 Object b = callBackend(a);

我希望框架首先捕获callBackend()给定参数 a 返回的 b,然后当我稍后进行试运行时说“嘿,给定 a 这个调用应该返回 b”。a 和 b 的值将相同(如果不是,我们将重新运行记录步骤)。

我可以重写提供 API 的类,因此所有用于捕获的方法调用都将通过我的代码(即不需要字节码检测来改变我无法控制的类的行为)。

我应该研究什么框架来做到这一点?


编辑:请注意赏金猎人应该提供实际代码来展示我寻找的行为。

4

9 回答 9

7

实际上,您可以使用代理模式构建这样的框架或模板。在这里,我解释了如何使用动态代理模式来做到这一点。这个想法是,

  1. 编写一个代理管理器来按需获取 API 的记录器和回放器代理
  2. 编写一个包装类来存储您收集的信息,并实现该包装类的方法hashCode和方法,以便从类似的数据结构equals中进行有效查找。Map
  3. 最后使用记录器代理进行记录和回放代理用于回放目的。

录音机的工作原理:

  1. 调用真正的 API
  2. 收集调用信息
  3. 在预期的持久性上下文中持久化数据

回放器的工作原理:

  1. 收集方法信息(方法名、参数)
  2. 如果收集的信息与之前记录的信息匹配,则返回之前收集的返回值。
  3. 如果返回值不匹配,则保留收集到的信息(如您所愿)。

现在,让我们看一下实现。如果您的 APIMyApi如下所示:

public interface MyApi {
    public String getMySpouse(String myName);
    public int getMyAge(String myName);
    ...
}

现在我们将记录并重放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;
            }
        }
    }

如果你想记录方法调用,你只需要得到一个 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
    ...

你完成了!

编辑: 记录器和回放器的基本步骤可以按照上述方式完成。现在由您决定如何使用或执行这些步骤。您可以在记录器和回放器代码块中做任何您想做的事情和您喜欢的任何事情,然后选择您的实现!

于 2013-05-12T16:55:29.660 回答
4

我应该先说我在 Yves Martin 的回答中分享了一些担忧:这样的系统可能会令人沮丧,并且最终没有乍看起来那么有用。

也就是说,从技术角度来看,这是一个有趣的问题,我不能不去尝试。我整理了一个要点,以一种相当通用的方式记录方法调用。在那里定义的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

您可以想象在将 API 对象CallLoggingProxy.create传递给您的测试方法之前对其进行包装,然后捕获数据,并使用您最喜欢的序列化系统将其持久化。稍后,当您想要运行测试时,您可以加载数据备份,基于数据创建一个新实例CallLoggingProxy.replay,然后将其传递给您的方法。

使用JavassistCallLoggingProxy编写,因为 Java 的本机仅限于针对接口工作。这应该涵盖一般用例,但要记住一些限制:Proxy

  • 声明的类final不能被此方法代理。(不容易修复;这是系统限制)
  • 要点假设方法的相同输入将始终产生相同的输出。(更容易修复;ReplayInfo需要跟踪每个输入的调用序列,而不是单个输入/输出对。)
  • 要点甚至不是远程线程安全的(很容易修复;只需要一点思考和努力)

显然,要点只是概念证明,因此也没有经过非常彻底的测试,但我相信一般原则是合理的。也有可能有一个更完善的框架来实现这种目标,但如果确实存在这样的事情,我不知道。

如果您决定继续使用重放方法,那么希望这足以为您提供可能的工作方向。

于 2013-05-11T23:13:10.303 回答
3

几个月前,在计划对大型应用程序进行大量技术重构时,我对非回归测试也有同样的需求……我没有发现任何可用的框架

事实上,重放可能特别困难,并且可能只在特定的上下文中工作 - 没有(或很少)具有标准复杂性的应用程序可以真正被视为无状态。在使用关系数据库测试持久性代码时,这是一个常见问题。为了相关,必须恢复完整的系统初始状态,并且每个重放步骤必须以相同的方式影响全局状态。当系统状态被分配到数据库、文件、内存等部分时,这将成为一个挑战……让我们猜猜如果在某处使用从系统时钟获取的时间戳会发生什么!

所以更实用的选择是只记录……然后为后续运行进行巧妙的比较。

根据您计划的运行次数,在应用程序上进行人工驱动的会话可能就足够了,或者您必须投资一个自动化场景,让机器人玩您的应用程序用户界面。

先记录一下:可以使用动态代理接口或者切面编程来拦截方法调用,捕获调用前后的状态。这可能意味着:转储相关的数据库表,复制一些文件,以 XML 等文本格式序列化 Java 对象。

然后将此参考捕获与新运行进行比较。应该调整此比较以从每个状态中排除任何不相关的元素,如行标识符、时间戳、文件名......仅比较后端附加值大放异彩的数据。

最后没有什么真正的标准,通常一些特定的脚本和代码可能足以实现目标:检测尽可能多的错误并尝试防止意外的副作用。

于 2013-05-11T21:17:13.223 回答
3

这可以通过AOP,面向方面的编程来完成。它允许通过字节码操作来拦截方法调用。做一些搜索示例。

在一种情况下,这可以进行录制,在另一种情况下可以重放。

指针:维基百科、AspectJ、Spring AOP。

不幸的是,在 java 语法之外移动了一点,一个简单的例子可以更好地在别处寻找。有解释。

也许与单元测试/一些模拟测试框架结合使用记录数据进行离线测试。

于 2013-05-12T00:09:45.390 回答
2

你可以看看'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));

在您可以多次重播每个测试之后,它将返回您输入的数据。

于 2013-05-13T08:28:41.283 回答
1
// pseudocode
class LogMethod {
   List<String> parameters;
   String method;
   addCallTo(String method, List<String> params):
       this.method = method;
       parameters = params;
   }
}

在您的测试方法中的每个调用之前都有一个列表LogMethods并调用。new LogMethod().addCallTo()

于 2013-05-11T21:25:07.453 回答
1

回放 API 调用的想法听起来像是事件溯源模式的用例。Martin Fowler 在这里有一篇很好的文章。这是一个很好的模式,将事件记录为一系列对象,然后存储,然后您可以根据需要重播事件序列。

有一个使用 Akka 的名为Eventsourced的模式实现,它可以帮助您构建所需的系统类型。

于 2013-05-12T12:49:15.617 回答
1

几年前我也遇到过类似的问题。上述解决方案均不适用于非纯函数(无副作用)的方法。我认为主要任务是:

  • 如何提取记录对象的快照(不仅限于实现的对象Serializable
  • 如何以可读的方式生成序列化表示的测试代码(不仅限于 bean、原语和集合)

所以我不得不走我自己的路——用testrecorder

例如,给定:

ResultObject b = callBackend(a);

...

ResultObject callBackend(SourceObject source) {
  ...
}

您只需要像这样注释方法:

@Recorded
ResultObject callBackend(SourceObject source) {
  ...
}

并使用 testrecorder 代理启动您的应用程序(应该记录的应用程序)。Testrecorder 将为您管理所有任务,例如:

  • 序列化参数、结果、状态、异常(完整的对象图!)
  • 为对象构造和对象匹配找到可读的表示
  • 从序列化数据生成测试
  • 您可以将记录扩展到全局变量、带注释的输入和输出

测试示例如下所示:

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
}
于 2017-10-11T05:36:36.420 回答
-3

如果我正确理解您的问题,您应该尝试 db4o。

您将使用 db4o 存储对象并稍后恢复到模拟和 JUnit 测试。

于 2013-05-11T20:08:05.437 回答