mock-server / mockserver

MockServer enables easy mocking of any system you integrate with via HTTP or HTTPS with clients written in Java, JavaScript and Ruby. MockServer also includes a proxy that introspects all proxied traffic including encrypted SSL traffic and supports Port Forwarding, Web Proxying (i.e. HTTP proxy), HTTPS Tunneling Proxying (using HTTP CONNECT) and SOCKS Proxying (i.e. dynamic port forwarding).
http://mock-server.com
Apache License 2.0
4.6k stars 1.07k forks source link

Support method / object callbacks instead of class name #120

Closed JanGurda closed 6 years ago

JanGurda commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

JanGurda commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

dvmahida commented 7 years ago

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

jamesdbloom commented 7 years ago

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

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 commented 7 years ago

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

lcintrat commented 7 years ago

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 commented 6 years ago

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

jamesdbloom commented 6 years ago

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.