Record method calls in one session for replaying in future test sessions?

I have a backend system which we use a third-party Java API to access from our own applications. I can access the system as a normal user along with other users, but I do not have godly powers over it.

Hence to simplify testing I would like to run a real session and record the API calls, and persist them (preferably as editable code), so we can do dry test runs later with API calls just returning the corresponding response from the recording session - and this is the important part - without needing to talk to the above mentioned backend system.

So if my application contains line on the form:

 Object b = callBackend(a);

I would like the framework to first capture that callBackend() returned b given the argument a, and then when I do the dry run at any later time say "hey, given a this call should return b". The values of a and b will be the same (if not, we will rerun the recording step).

I can override the class providing the API so all the method calls to capture will go through my code (i.e. byte code instrumentation to alter behavior of classes outside my control is not necessary).

What framework should I look into to do this?


EDIT: Please note that bounty hunters should provide actual code demonstrating the behavior I look for.

Upvotes: 41

Views: 5395

Answers (9)

CoronA
CoronA

Reputation: 8075

I had a similar problem some years ago. None of the above solutions would have worked for methods that are not pure functions (side effect free). The major task is in my opinion:

  • how to extract a snapshot of the recorded object(s) (not only restricted to objects implementing Serializable)
  • how to generate test code of a serialized representation in a readable way (not only restricted to beans, primitives and collections)

So I had to go my own way - with testrecorder.

For example, given:

ResultObject b = callBackend(a);

...

ResultObject callBackend(SourceObject source) {
  ...
}

you will only have to annotate the method like this:

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

and start your application (the one that should be recorded) with the testrecorder agent. Testrecorder will manage all tasks for you, such as:

  • serializing arguments, results, state of this, exceptions (complete object graph!)
  • finding a readable representation for object construction and object matching
  • generating a test from the serialized data
  • you can extend recordings to global variables, input and output with annotations

An example for the test will look like this:

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
}

Upvotes: 1

Sazzadur Rahaman
Sazzadur Rahaman

Reputation: 7116

Actually You can build such framework or template, by using proxy pattern. Here I explain, how you can do it using dynamic proxy pattern. The idea is to,

  1. Write a proxy manager to get recorder and replayer proxies of API on demand!
  2. Write a wrapper class to store your collected information and also implement hashCode and equals method of that wrapper class for efficient lookup from Map like data structure.
  3. And finally use recorder proxy to record and replayer proxy for replaying purpose.

How recorder works:

  1. invokes the real API
  2. collects the invocation information
  3. persists data in expected persistence context

How replayer works:

  1. Collect the method information (method name, parameters)
  2. If collected information matches with previously recorded information then return the previously collected return value.
  3. If returned value does not match, persist the collected information (As you wanted).

Now, lets look at the implementation. If your API is MyApi like bellow:

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

Now we will, record and replay the invocation of public String getMySpouse(String myName). To do that we can use a class to store the invocation information like bellow:

    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!
        }
    }

Now Here comes the main part, The RecordReplyManager. This RecordReplyManager gives you proxy object of your API , depending on your need of recording or replaying.

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

If you want to record the method invocation, all you need is getting an API proxy like bellow:

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

And to replay all you need:

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

And You are Done!

Edit: The basic steps of recorder and replayers can be done in above mentioned way. Now its upto you, that how you want to use or perform those steps. You can do what ever you want and whatever you like in the recorder and replayer code blocks and just choose your implementation!

Upvotes: 7

Dmitry Zagorulkin
Dmitry Zagorulkin

Reputation: 8548

you could look into 'Mockito'

Example:

//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));

after you could replay each test more times and it will return data that you put in.

Upvotes: 2

dfreeman
dfreeman

Reputation: 2834

I should prefix this by saying I share some of the concerns in Yves Martin's answer: that such a system may prove frustrating to work with and ultimately less helpful than it would seem at first blush.

That said, from a technical standpoint, this is an interesting problem, and I couldn't not take a go at it. I put together a gist to log method calls in a fairly general way. The CallLoggingProxy class defined there allows usage such as the following.

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

You could imagine wrapping your API object with CallLoggingProxy.create prior to passing it into your testing methods, capturing the data afterwards, and persisting it using whatever your favorite serialization system happens to be. Later, when you want to run your tests, you can load the data back up, create a new instance based on the data with CallLoggingProxy.replay, and passing that into your methods instead.

The CallLoggingProxy is written using Javassist, as Java's native Proxy is limited to working against interfaces. This should cover the general use case, but there are a few limitations to keep in mind:

  • Classes declared final can't be proxied by this method. (Not easily fixable; this is a system limitation)
  • The gist assumes the same input to a method will always produce the same output. (More easily fixable; the ReplayInfo would need to keep track of sequences of calls for each input instead of single input/output pairs.)
  • The gist is not even remotely threadsafe (Fairly easily fixable; just requires a little thought and effort)

Obviously the gist is simply a proof of concept, so it's also not been very thoroughly tested, but I believe the general principle is sound. It's also possible there's a more fully baked framework out there to achieve this sort of goal, but if such a thing does exist, I'm not aware of it.

If you do decide to continue with the replay approach, then hopefully this will be enough to give you a possible direction to work in.

Upvotes: 4

Leesrus
Leesrus

Reputation: 1115

The idea of playing back the API calls sounds like a use case for the event sourcing pattern. Martin Fowler has a good article on it here. This is a nice pattern that records events as a sequence of objects which are then stored, you can then replay the sequence of events as required.

There is an implementation of this pattern using Akka called Eventsourced, which may help you build the type of system you require.

Upvotes: 1

Joop Eggen
Joop Eggen

Reputation: 109557

This can be done with AOP, aspect oriented programming. It allows to intercept method calls by byte code manipulation. Do a bit of search for examples.

In one case this can do recording, in the other replaying.

Pointers: wikipedia, AspectJ, Spring AOP.

Unfortunately one moves a bit outside the java syntax, and a simple example can better be sought elsewhere. With explanation.

Maybe combined with unit tests / some mocking test framework for offline testing with recorded data.

Upvotes: 3

hd1
hd1

Reputation: 34657

// pseudocode
class LogMethod {
   List<String> parameters;
   String method;
   addCallTo(String method, List<String> params):
       this.method = method;
       parameters = params;
   }
}

Have a list of LogMethods and call new LogMethod().addCallTo() before every call in your test method.

Upvotes: 1

Yves Martin
Yves Martin

Reputation: 10361

I had the same needs some months ago for non-regression testing when planning a heavy technical refactoring of a large application and... I have found nothing available as a framework.

In fact, replaying may be particularly difficult and may only work in a specific context - no (or few) application with a standard complexity can be really considered as stateless. It is a common problem when testing persistence code with a relational database. To be relevant, the complete system initial state must be restored and each replay step must impact the global state the same way. It becomes a challenge when a system state is distributed into pieces like databases, files, memory... Let's guess what happens if a timestamp taken from a system's clock is used somewhere !

So a more pratical option is to only record... and then do a clever comparison for subsequent runs.

Depending of the number of runs you plan, a human-driven session on the application may be enough, or you have to investing in an automated scenario in a robot playing with your application user interface.

First to record: you can use dynamic proxy interface or aspect programming to intercept method call and to capture state before and after invocation. It may mean: dump concerned database tables, copy some files, serialize Java objects in text format like XML.

Then compare this reference capture with a new run. This comparison should be tuned to exclude any irrelevant elements from each piece of state, like row identifiers, timestamps, file names... to only compare data where your backend's added value shines.

Finally nothing really standard, and often a few specific scripts and codes may be enough to achieve the aim: detect as much errors as possible and try to prevent non-expected side-effects.

Upvotes: 3

Aquarius Power
Aquarius Power

Reputation: 3985

If I understood you question correctly, you should try db4o.

You will store the objects with db4o and restore later to mock and JUnit tests.

Upvotes: -3

Related Questions