Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support method / object callbacks instead of class name #120

Closed
JanGurda opened this issue Apr 22, 2015 · 17 comments
Closed

Support method / object callbacks instead of class name #120

JanGurda opened this issue Apr 22, 2015 · 17 comments

Comments

@JanGurda
Copy link

Thanks for nice piece of software.
I have just started using MockServer I love it however I feel I miss one feature. While mocking I'm able to specify HttpCallback which will be executed on server invocation. However HttpCallback may contain only class name. For some use cases I would like something more dynamic. I wonder if you considered allowing to put instance rather than class into HttpCallback so user could define behavior more dynamically.

That would be more similar to Mockito's when(something).thenAnswer(code_to_execute_here)

What do you think?

@jamesbloomnektan
Copy link
Collaborator

The problem with this is that the expectation is serialised to the server using JSON. Typically the server runs as a separate process and so has no access to local objects. So currently there is no way to support executing an instance object.

What you can do as a work around is make sure the MockServer is running in the same JVM by using the API such as ServerAndClient then use static methods. However, this is pretty much a hack that I don't recommend as you would have no test separation.

I'm going to close this ticket as it is not possible due to limitation in Java.

@jamesbloomnektan
Copy link
Collaborator

However I could implement this using web sockets, so I'll re-open this ticket and implement it when I get a chance.

@JanGurda
Copy link
Author

You are right. I missed the fact that everything goes through the wire even when we use junit Rule and server runs inside the same JVM process. Websockets is nice solution for that. We simply need a way to invoke code on MockServerClient's side and pass created response back to server. I see also other option here (very similar to websockets) - expose some kind of server endpoint on client side (could be HTTP server).

When API user records behavior (creates expectations) with instance of given interface (similar to ExpectationCallback) client could assign unique identifier and map this unique identifier to that expectation instance. That unique id could be later passed to server and then server finds that should invoke expectation it calls client giving id and gets HttpResponse back. This is how I see that now.

Thank you for quick response.

@jamesbloomnektan
Copy link
Collaborator

For now I think web sockets is the best approach as it works really nicely in Netty which is used for both the client and the server. I've previously implemented web sockets with Netty before and they provide a really solid and simple implementation.

@matthurne
Copy link

When using MockServer via the JUnit MockServerRule, is everything running in the same JVM such that passing a ExpectationCallback instance would be possible? In Spock tests, this would be quite powerful, as a Closure could be used to implement the callback...

@jamesdbloom
Copy link
Collaborator

Although when using the MockServerRule everything is running in the same JVM. The MockServer is running in a separate set of threads managed by Netty and so it wouldn't be immediately straight forward to pass a closure in. In the next couple of weeks I'll start implementing a WebSocket approach as this would likely be the simplest solution even in the same JVM and would additionally work when the MockServer was in another JVM. I just need to improve the Ruby client and finish improving the web site / documentation then this issue is next on the list.

@Fuud
Copy link

Fuud commented Jul 7, 2015

May be we can also extend HttpCallback to include some kind of tag-id (or callback-id) - it will help to distinguish requests on server side with same callback (and callback will be able to use static map to get proper responce).

@jamesdbloom
Copy link
Collaborator

Yes exactly the server may need to maintain a list (probably UUIDs) against WebSocket to know which socket to call back on. Alternatively it may also be possible avoid any hash map / lookup table and if the objects can be structured correctly.

I'm just working on updating the documentation, once this is completed and the ruby client is fixed this is the next item on the list, and is likely to be completed in the next month or so.

@Fuud
Copy link

Fuud commented Jul 7, 2015

Some additional information about my use-cases. Maybe it will be helpful (or just interesting :) )
I use gridkit (https://github.com/gridkit/nanocloud) to instantiate MockServer on a per test basis.
Sometimes I need to do some external things on http-request (drop some other service, change state of components, etc).
On different http-requests it can be different external tasks.
Currently I forced to create new callback classes for each task: I am using javassist to create classes with common superclass and static Map<Class, Runnable> to specify callback behavour on a per-class basis.

public class ExpectationCallbackImpl implements ExpectationCallback {
    private static final Map<Class, RequestAndCallbackAndResponse> requestToResponse = new ConcurrentHashMap<>();

    @Override
    public HttpResponse handle(HttpRequest httpRequest) {
        final RequestAndCallbackAndResponse callbackAndResponse = requestToResponse.get(this.getClass());
        callbackAndResponse.callback.run();
        return callbackAndResponse.getResponse();
    }
public void invokeCallbackAndRespond(final HttpResponse httpResponse, final RemoteRunnable callback) {
            final ExpectationCallbackImpl.RequestAndCallbackAndResponse remoteCallback = new ExpectationCallbackImpl.RequestAndCallbackAndResponse(request, httpResponse, callback);

            final String callbackClass = node.exec(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    // create new class
                    ProxyFactory factory = new ProxyFactory();
                    factory.setSuperclass(ExpectationCallbackImpl.class);
                    Class c = factory.createClass();

                    // register callback
                    ExpectationCallbackImpl.addCallback(c, remoteCallback);

                    //return class name
                    return c.getName();
                }
            });
            client.when(request).callback(new HttpCallback().withCallbackClass(callbackClass));
        }

@jamesdbloom
Copy link
Collaborator

thanks code examples are always used to fully understand how people use things.

The original use case for the callback class was completely different and it only required a single static class, but it should be possible to extend it to cover your requirements.

I'll implement it so the object instance you pass in will have to implement a Single Abstract Methods (SAM) interface (i.e. interface with a single method). This will mean those using Java 6+ can just implement the interface and those using Java 8+ can pass in a closure if they like. Even though MockServer is compiled with Java 6 for maximum support this approach should make the API nice for those using Java 8.

@jamesdbloom
Copy link
Collaborator

Note: see #160 and make sure the solution uses WebSockets so that function callbacks can be used from JavaScript in browser or node.js

@jamesdbloom jamesdbloom changed the title Allow client to put inside HttpCallback instance rather than class name Support method / object callbacks instead of class name Sep 21, 2015
jamesdbloom added a commit that referenced this issue Sep 22, 2015
…for method callbacks - only covers the web socket part so far but not yet integrated into Java or JavaScript clients
@dvmahida
Copy link

@jamesdbloom any update on this ? are you planning to complete this anytime soon ?

@jamesdbloom
Copy link
Collaborator

The dynamic callback have been completed, except the following items:

  • ruby client support (although the ruby client will be completely re-written, asap)
  • performance testing (not tested supporting 100s of simultaneous dynamic callback handlers, this would result in 100s of open WebSockets and need to confirm this will scale safely)
  • documenting feature on website
  • adding new examples to the mockserver-examples project

Neither of these two remaining items will change the API and so it should be safe to use this functionality. The Java, browser javascript and node javascript clients all support it now, as follows:

Java

import org.junit.Rule;
import org.junit.Test;
import org.mockserver.client.server.MockServerClient;
import org.mockserver.junit.MockServerRule;
import org.mockserver.mock.action.ExpectationCallback;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.mockserver.matchers.Times.exactly;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.StringBody.exact;

/**
 * @author jamesdbloom
 */
public class DynamicMethodCallback {

    @Rule
    public MockServerRule mockServerRule = new MockServerRule(this);

    private MockServerClient mockServerClient;

    @Test
    public void doSomethingJava7() {
        mockServerClient
                .when(
                        request()
                                .withMethod("POST")
                                .withPath("/login")
                                .withQueryStringParameter(
                                        "returnUrl", "/account"
                                )
                                .withCookie(
                                        "sessionId", "2By8LOhBmaW5nZXJwcmludCIlMDAzMW"
                                )
                                .withBody(exact("{username: 'foo', password: 'bar'}")),
                        exactly(1)
                )
                .callback(
                        new ExpectationCallback() {
                            public HttpResponse handle(HttpRequest httpRequest) {
                                return response()
                                        .withStatusCode(401)
                                        .withHeader(
                                                "Content-Type", "application/json; charset=utf-8"
                                        )
                                        .withHeader(
                                                "Cache-Control", "public, max-age=86400"
                                        )
                                        .withBody("{ message: 'incorrect username and password combination' }")
                                        .withDelay(SECONDS, 1);
                            }
                        }
                );
    }

    @Test
    public void doSomethingJava8() {
        mockServerClient
                .when(
                        request()
                                .withMethod("POST")
                                .withPath("/login")
                                .withQueryStringParameter(
                                        "returnUrl", "/account"
                                )
                                .withCookie(
                                        "sessionId", "2By8LOhBmaW5nZXJwcmludCIlMDAzMW"
                                )
                                .withBody(exact("{username: 'foo', password: 'bar'}")),
                        exactly(1)
                )
                .callback(
                        (HttpRequest httpRequest) -> response()
                                .withStatusCode(401)
                                .withHeader(
                                        "Content-Type", "application/json; charset=utf-8"
                                )
                                .withHeader(
                                        "Cache-Control", "public, max-age=86400"
                                )
                                .withBody("{ message: 'incorrect username and password combination' }")
                                .withDelay(SECONDS, 1)
                );
    }
}

Javascript

mockServerClient("localhost", 1080).mockWithCallback(
    {
        'method': 'GET',
        'path': '/two'
    }, 
    function (request) {
        
        // some callback logic
        
        if (request.method === 'GET' && request.path === '/two') {
            return {
                'statusCode': 202,
                'body': 'two'
            };
        } else {
            return {
                'statusCode': 406
            };
        }
    }
)
.then(
    function () {

        // expectation setup now test something

    }, 
    function (error) {
        
        // failed to setup expectation
        
    }
);

@jamesdbloom
Copy link
Collaborator

FYI your'll need to use mockserver-netty version 3.10.7, mockserver-grunt@1.0.41 or the latest docker container

@lcintrat
Copy link

lcintrat commented Jul 1, 2017

Hello @jamesdbloom,

Your previous DynamicMethodCallback example for Java works fine for me. However, if I add a subsequent verification to your example, it fails:

mockServerClient
                .verify(
                        request()
                                .withMethod("POST")
                                .withPath("/login"),
                        VerificationTimes.once()
                );

More over, mockServerClient.retrieveRecordedRequests(null) returns an empty array.

Is it the expected behavior?

@jamesdbloom
Copy link
Collaborator

That is not expected behaviour and I will submit a fix very soon.

jamesdbloom added a commit that referenced this issue Nov 13, 2017
…not record to the log and so broken the verify functionality
@jamesdbloom
Copy link
Collaborator

The bug you raised is now fixed, closing this ticket and migrating the remaining documentation part to #115 which is the next highest priority issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants