Closed JanGurda closed 6 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.
However I could implement this using web sockets, so I'll re-open this ticket and implement it when I get a chance.
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.
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.
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...
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.
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).
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.
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));
}
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.
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 any update on this ? are you planning to complete this anytime soon ?
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:
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)
);
}
}
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
}
);
FYI your'll need to use mockserver-netty version 3.10.7, mockserver-grunt@1.0.41 or the latest docker container
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?
That is not expected behaviour and I will submit a fix very soon.
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.
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?