May 6, 2016

Custom Restito Sequenced Stub Action

In this post I show how to write custom Restito Action to mock a single REST endpoint to response differently even is subsequent requests are identical.

Restito is lightweight testing framework that provides a Java DSL to:
  • mimic REST server behavior,
  • perform verification against happened calls.
It is based on Grizzly HTTP Server. See developer's guide for more details about all supported features.

Let's say I want to test business login of application that calls following REST API:
  1. Gets list of all open comments (GET /comments),
  2. Archives each comment that is older than a month (DELETE /comments/{id}),
  3. Lists all remaining open comments (GET /comments).

import java.util.List;

import com.xebialabs.restito.builder.ensure.EnsureHttp;
import com.xebialabs.restito.support.junit.StartServer;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import static com.xebialabs.restito.builder.stub.StubHttp.whenHttp;
import static com.xebialabs.restito.semantics.Action.contentType;
import static com.xebialabs.restito.semantics.Action.status;
import static com.xebialabs.restito.semantics.Action.stringContent;
import static com.xebialabs.restito.semantics.Condition.delete;
import static com.xebialabs.restito.semantics.Condition.get;
import static org.glassfish.grizzly.http.util.HttpStatus.NO_CONTENT_204;
import static org.glassfish.grizzly.http.util.HttpStatus.OK_200;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;

public class MyAppTest {

    private static final String GET_COMMENTS_BEFORE =
              "{\"comments\":[\n"
            + "    {\"id\" : 1, \"content\" : \"Happy New Year ...\", \"date\" : \"2016-01-01\"},\n"
            + "    {\"id\" : 2, \"content\" : \"Happy Valentine...\", \"date\" : \"2016-02-14\"},\n"
            + "    {\"id\" : 3, \"content\" : \"I like Restito ...\", \"date\" : \"2016-05-06\"}\n"
            + "]}";
    private static final String GET_COMMENTS_AFTER =
              "{\"comments\":[\n"
            + "    {\"id\" : 3, \"content\" : \"I like Restito ...\", \"date\" : \"2016-05-06\"}\n"
            + "]}";

    @Rule
    public StartServer startServer = new StartServer();

    @Test
    public void testArchiveOldComments() {
        // 1. Stub for: Gets list of all open comments (GET /comments):
        whenHttp(startServer.getServer()).
                match(get("/comments")).
                then(status(OK_200),
                     contentType("application/json"),
                     stringContent(GET_COMMENTS_BEFORE)).
                mustHappen();
        // 2. Stub for: Archives each comment that is older than a month (DELETE /comments/{id}):
        whenHttp(startServer.getServer()).
                match(delete("/comments/1")).
                then(status(NO_CONTENT_204)).
                mustHappen();
        whenHttp(startServer.getServer()).
                match(delete("/comments/2")).
                then(status(NO_CONTENT_204)).
                mustHappen();
        // 3. Stub for: Lists all remaining open comments (GET /comments):
        whenHttp(startServer.getServer()).
                match(get("/comments")).
                then(status(OK_200),
                     contentType("application/json"),
                     stringContent(GET_COMMENTS_AFTER)).
                mustHappen();

        // call business logic
        MyApp myApp = new MyApp();
        List<Comment> comments = myApp.archiveOldComments();

        // remaining 1 comment:
        assertThat(comments, hasSize(1));
        // ensure that each stubs has been invoked:
        EnsureHttp.ensureHttp(startServer.getServer()).gotStubsCommitmentsDone();
    }

}

But this test fails because DELETE stubs are not invoked.

MyAppTest > testArchiveOldComments FAILED
    java.lang.AssertionError at MyAppTest.java:72

Expected stub com.xebialabs.restito.semantics.Stub@775449c1 to be called 1 times, called 0 times instead
java.lang.AssertionError: Expected stub com.xebialabs.restito.semantics.Stub@775449c1 to be called 1 times, called 0 times instead

Note: You can see the error output is not comprehensive. It is known issue, see https://github.com/mkotsur/restito/issues/45.

The reason why DELETE methods are not invoked is that the first GET call does not return JSON with 3 comments but just 1 (JSON in GET_COMMENTS_AFTER String). It means that the second GET /comments stub overrides the first one. Or, the first stub that returns JSON with 3 comments (GET_COMMENTS_BEFORE String) is ignored because the match Conditions for both stubs are identical.

Unfortunately Restito does not support directly this use case. Fortunately you can create custom Action or Condition. Described use-case can be solved by custom Action.

import java.util.LinkedList;
import java.util.Queue;

import com.xebialabs.restito.semantics.Action;
import com.xebialabs.restito.semantics.Function;
import org.glassfish.grizzly.http.server.Response;

public class SequencedAction implements Function<Response, Response> {
    private Queue<Action> queue = new LinkedList<>();

    public SequencedAction(Action... actions) {
        for (Action action : actions) {
            this.queue.offer(action);
        }
    }

    @Override
    public Response apply(Response input) {
        Action next = queue.poll();
        if (next != null) {
            input = next.apply(input);
        }
        return input;
    }
}

Custom SequencedAction keeps a queue of not yet processed Actions and polls just one available Action from the queue in apply method. I.e. subsequent SequencedAction.apply invocation behaves differently. If the queue is empty the SequencedAction.apply does nothing like noop action.

So now I can update test method to use custom action.

@Test
public void testArchiveOldComments() {
    // 1. Stub for both: Gets list of all open comments (GET /comments):
    whenHttp(startServer.getServer()).
            match(get("/comments")).
            then(status(OK_200),
                 contentType("application/json"),
                 // use custom action:
                 Action.custom(new SequencedAction(
                         // the first get: returns 3 comments
                         stringContent(GET_COMMENTS_BEFORE),
                         // the second get: returns 1 comment
                         stringContent(GET_COMMENTS_AFTER)
                 ))).
            // 2 actions in sequence:
            mustHappen(2);
    // 2. Stub for: Archives each comment that is older than a month (DELETE /comments/{id}):
    whenHttp(startServer.getServer()).
            match(delete("/comments/1")).
            then(status(NO_CONTENT_204)).
            mustHappen();
    whenHttp(startServer.getServer()).
            match(delete("/comments/2")).
            then(status(NO_CONTENT_204)).
            mustHappen();

    // call business logic
    MyApp myApp = new MyApp();
    List<Comment> comments = myApp.archiveOldComments();

    // remaining 1 comment:
    assertThat(comments, hasSize(1));
    // ensure that each stubs has been invoked:
    EnsureHttp.ensureHttp(startServer.getServer()).gotStubsCommitmentsDone();
}

And the test is fixed.

You are right, it is a little bit too verbose to use custom action. So I have decided to contribute the sequenced action directly to Restito Action, method sequence, see or comment or vote for my GitHub pull request: https://github.com/mkotsur/restito/pull/49. 😇

Enjoy,
Libor

9 comments:

  1. Certsout.com provides authentic IT Certification exams preparation material guaranteed to make you pass in the first attempt. Download instant free demo & begin preparation. 810-440 Exam Practice Test

    ReplyDelete
  2. Friend, this web site might be fabolous, i just like it. Google pixel 2 XL Wireless Charging

    ReplyDelete
  3. . I've been related with both those that succeeded and those that fizzled. Each had interesting driving edge advancements. The thing that matters was opportunity. https://www.techpally.com/red-cross-first-aid-apps/

    ReplyDelete
  4. All the contents you mentioned in post is too good and can be very useful. I will keep it in mind, thanks for sharing the information keep updating, looking forward for more posts.Thanks Product Review Portal

    ReplyDelete
  5. This blog was extremely helpful. I really appreciate your kindness in sharing this with me and everyone else! Magento developers

    ReplyDelete
  6. I will do niche blog comment Just in 5$ .All comment relevant with your niche and UNIQUE .This off-page seo will increase your traffic and promote your business. custom writing

    ReplyDelete
  7. The particular software program is best supported on Windows platform with full support for approximately date edition of house windows like Home windows landscape. This software is download twitter videos generally designed with one objective and that is to be able to extend your working speed on this world that is shifting and working at such a speedy rate, thus serving to an individual to save lots of time.

    ReplyDelete
  8. I got so involved in this material that I couldn’t stop reading. I am impressed with your work and skill. Thank you so much.
    wowonder nulled

    ReplyDelete