1. What is Arquillian Warp?

Arquillian Warp fills the void between client-side and server-side testing. Warp extension allows you to write client-side test which asserts server-side logic.

Using Warp, you can initiate an HTTP request using a client-side testing tool such as WebDriver and, in the same request cycle, execute in-container server-side tests. This powerful combination lets you cover integration across client and server.

Warp effectively removes a need for mocking and opens new possibilities for debugging.

Warp has built-in support for the following frameworks:

  • Jakarta Servlet

  • Jakarta Faces

Warp version 2.0 (in development) is based on Jakarta EE 10 and requires JDK 11 or newer, while version 1.1 is based on Java EE 8.

2. Where to Use Warp?

Warp allows you to know as little or as much of the application under test as you want.

2.1. Gray-box Testing

Initially, Warp can be used from any black-box testing tool (like HttpClient, REST client, Selenium WebDriver, etc.). But it then allows you to hook into server’s request lifecycle and verify what happens inside the box (referred to as white-box testing). That is why we identify Warp as a gray-box testing framework.

2.2. Integration Testing

On the scale of granularity of a test, Warp fits best at integration level of testing with an overlap to functional testing. You can either test components, application API or functional behavior.

2.3. Technology Independence

No matter which client-side tool you use for emitting HTTP request or which server-side framework runs on the server, Warp allows you to assert and verify logic at the most appropriate place of client-server request lifecycle.

2.4. Use Cases

Using Warp is limited just by your imagination, as long as you have in mind facts we had described earlier.

Just to note some specific use cases, Warp can:

  • send a payload to a server

  • verify an incoming request

  • assert the state of a server context

  • verify that given event was fired during request processing

  • verify a completed response

  • send a payload to a client

2.5. Deploying Warp

Warp is a good fit for basically any stage of a project:

  • prototyping (TDD)

  • legacy projects

  • issue reproduction

  • debugging issue

Due to it’s ability to bring an arbitrary payload to a server and hook into server-lifecycle, we can use Warp in partially implemented projects. We needn’t have a database layer implemented in order to test UI logic. This is especially useful for projects based on loosely coupled components (e.g. Java EE’s CDI).

Since we can use Warp and start with white-box testing, we can reproduce the issue on the level of application’s interface (issue reproduction). Then we can dive deeper into internals of an application, which will not only allow us to reveal how it works, but also debug a system and assert the correct behavior.

2.6. Supported Tools and Frameworks

2.6.1. Cross-Protocol

Warp currently supports only HTTP protocol, but conceptually it can be used with any protocol where we are able to intercept client-to-server communication on both, the client and the server.

2.6.2. Client-Side Testing Tools

Warp supports any client-side tools if you are using them in a way that requests can be intercepted (in a case of HTTP protocol, you need to communicate through a proxy instead of direct communication with a server).

The samples of such tools:

  • URL#getResourceAsStream()

  • Apache HTTP Client

  • Selenium WebDriver

In order to use Warp, you should inject an @ArquillianResource URL, which points to the proxy automatically.

2.6.3. Frameworks

Warp currently focuses on frameworks based on Servlets API, but it provides special hooks and additional support for:

  • JSF

  • JAX-RS (REST)

  • Spring MVC

3. Getting Started

3.1. Setting Up A Project

Just add impl module to classpath and run test either from an IDE or Maven.

<dependency>
    <groupId>org.jboss.arquillian.extension</groupId>
    <artifactId>arquillian-warp</artifactId>
    <version>2.0.0.Final-SNAPSHOT</version>
    <type>pom</type>
</dependency>

or any framework-specific extension:

<dependency>
    <groupId>org.jboss.arquillian.extension</groupId>
    <artifactId>arquillian-warp-jsf</artifactId>
    <version>2.0.0.Final-SNAPSHOT</version>
</dependency>

Use the servlet protocol in arquillian.xml configuration:

<defaultProtocol type="Servlet 3.0"/>

3.2. Writing Warp Tests

To allow your test to use the Warp, place a @WarpTest annotation on the test class:

@RunWith(Arquillian.class)
@WarpTest
@RunAsClient
public class BasicTest {
}
Don’t forget to force Arquillian to run the test on a client with a @RunAsClient annotation.

3.2.1. Using Warp To Trigger The Client Action

You can use any HTTP client, such as WebDriver (driven by @Drone), to trigger the server logic:

@Drone
WebDriver browser;

Then use Warp utility class to run initiate method.

@Test
public void test() {

    Warp
       .initiate(new Activity() {

            public void perform() {
                browser.navigate().to(contextPath + "index.jsf");
            }})

       .inspect(new Inspection() {
            private static final long serialVersionUID = 1L;
        });
}

You need to provide Activity - the contract of this interface is that its perform method leads to triggering one or more HTTP requests against contextPath URL (injected by Arquillian).

Finally, in the inspect method, you need to provide object which implements Inspection interface. This interface provides contract for object which can execute server-side logic.

Don’t forget to provide serialVersionUID for Inspection objects.

3.2.2. Asserting Server State With Inspection

In the Inspection implementation, you can provide test methods annotated with lifecycle-test annotations:

  • @BeforeServlet

  • @AfterServlet

  • @BeforePhase

  • @AfterPhase

Simple assertion may look like:

new Inspection() {

    private static final long serialVersionUID = 1L;

    @Inject
    CDIBean cdiBean;

    @AfterPhase(RENDER_RESPONSE)
    public void test_initial_state() {
        assertEquals("John", cdiBean.getName());
    }
}
You can use dependency injection to bring the classes such as CDI beans, EJB beans, or any other resource supported by Arquillian.

4. How to Use Warp?

4.1. Project Setup

4.1.1. Setting Up Maven Dependencies

Following dependencies needs to be configured:

Warp depchain (does contain support just for Servlets API)
<dependency>
    <groupId>org.jboss.arquillian.extension</groupId>
    <artifactId>arquillian-warp</artifactId>
    <version>2.0.0.Final-SNAPSHOT</version>
    <type>pom</type>
</dependency>

or JSF-specific binding:

<dependency>
    <groupId>org.jboss.arquillian.extension</groupId>
    <artifactId>arquillian-warp-jsf</artifactId>
    <version>2.0.0.Final-SNAPSHOT</version>
</dependency>

4.1.2. Use Servlet Protocol

In arquillian.xml:

<defaultProtocol type="Servlet 3.0"/>

4.1.3. Create a Test

The test needs to be annotated with @WarpTest and needs to be run on a client (@RunAsClient).

It needs to contain WAR deployment which will be testable:

@RunWith(Arquillian.class)
@WarpTest
@RunAsClient
public class BasicTest {

    @Deployment(testable = true)
    public static WebArchive deployment() {
       ...
    }

    @Test
    public void test() {
      // now we can use Warp
      Warp
        ...
    }
}

4.2. Warp API

The Warp requires two inputs from the user:

  • what activity should be done in order to trigger a request

  • how the server processing should be inspected

That intuitively leads us to the simplest possible high-level API:

Warp
  .initiate(Activity)
  .inspect(Inspection);
This is a so called single-request execution API. In this API, the first observed request is inspected.

The result of single-request execution API is an Inspection returned from a server:

CustomInspection result =
  Warp
  .initiate(Activity)
  .inspect(CustomInspection);

4.3. Activity

The Activity is a manipulation with a client which leads to communication (emits a HTTP request). You are unlimited in tooling choices, but you should always use an URL provided by the Arquillian:

@ArquillianResource
URL contextPath;

So we can e.g. use simplest possible Warp activity:

Warp
  .initiate(new Activity() {
    InputStream connection = contextPath.getResourceAsStream();
    connection.open();
  }

You can use whatever tool you want here (e.g. HttpClient, Selenium WebDriver, etc.).

Warp
  .initiate(new Activity() {
    webdriver.navigate().to(contextPath.toString());
  }

4.4. Request Observers

Since HTTP clients can generally emit more than one request at a time, we may need to limit what will Warp react on.

We can achieve that using Warp API extended for Request Observing:

Warp
  .initiate(Activity)
  .observe(Observer)
  .inspect(Inspection);

In case of HTTP requests, we want to use HttpObserver specifically. We can implement our own or use predefined fluent API for observer specification.

4.4.1. Fluent API for HTTP Request Observing

In order to make observer definitions as accessible and readable as possible, there is fluent API for request observer specification:

import static ...request;
...
Warp
  .initiate(Activity)
  .observe(request().url().contains("index.html"))
  .inspect(Inspection);

4.5. Request Groups

An Activity inspected by Warp can lead not only to just one request, but also several parallel or sequential requests.

In order to test multiple requests during one Warp execution, you can use Request Groups API:

Warp
  .initiate(Activity)
  .group(id1)
    .observe(Observer)
    .inspect(Inspection...)
  .group(id2)
    .observe(Observer)
    .inspect(Inspection...)
  .execute();
The identifiers (id1, id2) are optional, they serve just the purpose of identification of a group in a result.
Don’t forget to use .execute() at the end of a Warp specification - it ends a specification and starts Warp execution process.
The observers need to be used for each of the groups, since no request can belong to more than one group.

4.5.1. Multiple Requests per Group

The Request Group API can be used also for verification of multiple requests with same inspection:

Warp
  .initiate(Activity)
  .group()
    .count(2)
    .inspect(Inspection)
  .execute();

The definition above expresses that there are two similar requests expected which will be inspected by given Inspection.

4.5.2. Group Identifiers

For identification of a group, you can use arbitrary identifier (either primitive value or an object with correctly implemented equivalence), so e.g.:

   .group("group1")
   .group(1)
   .group(object);
   .group() // identifier is optional

4.5.3. Result of Warp Group Execution

As a result of non-trivial (not single-request) execution of a Warp is a WarpResult.

WarpResult result =
  Warp
  .initiate(Activity)
  .group(id)
    .inspect()
  .execute();

Once you have provided an identifier for a group, you can retrieve a WarpGroup result:

WarpGroup group = result.getGroup(id);

The WarpGroup result can be used to:

  • obtain an inspection returned from the server

  • verify the state of responses

4.6. Inspection

An initiated request can be inspected during its execution using

.inspect(new Inspection() {
  ...
})

An Inspection object

  • is serialized on a client and sent to a server

  • can bear a payload

  • can contain lifecycle hooks which triggers verification logic

  • can be enriched using dependency injection

4.7. Lifecycle Hooks

Once an inspection is transferred to a server VM, it can be used to assert a state.

In order to define, when the inspection should execute its logic, one needs to use so called request lifecycle hooks.

Most basic lifecycle hooks are:

@BeforeServlet

executed before a servlet request enters servlet processing (in a servlet’s filter chain)

@AfterServlet

executed after a servlet request leaves servlet processing (in a servlet’s filter chain)

The lifecycle hooks are used to invoke methods:

Warp
  .initiate(Activity)
  .inspect(new Inspection() {

    @BeforeServlet
    public void verifyRequest() {
      ...
    }
  });

There are also hooks specific for each of the supported frameworks.

4.7.1. JavaServer Faces Lifecycle Hooks

JSF allows to hook into the request lifecycle:

@BeforePhase(Phase)

executed before a given JSF lifecycle phase is processed

@AfterPhase(Phase)

executed after a given JSF lifecycle phase is processed

4.8. Dependency Injection

In order to test server-side state effectively, the Inspection can be enriched with any of the dependencies injectable by Arquillian:

  • @Inject for CDI beans

  • @EJB for EJB beans

  • @ArquillianResource for Arquillian Resources

  • @ManagedProperty("#{expression}") for beans from Expression Language context (JSF)

The injection can be either at class-level or method-level:

Warp
  .initiate(Activity)
  .inspect(new Inspection() {

    @ArquillianResource
    private HttpServletRequest request;

    @ManagedProperty("#{user.friends}")
    private Set<User> friends;

    @AfterPhase(INVOKE_APPLICATION)
    public void verifyRequest(@CurrentUser User user) {
      ...
    }
  });
As you can see in the sample above, CDI beans can be injected at method-level even though you don’t specify @Inject.

4.8.1. Dependencies Specific to Servlets

Following resources can be injected using @ArquillianResource:

ServletRequest

HttpServletRequest

ServletResponse

HttpServletResponse

4.8.2. Dependencies Specific to JavaServer Faces

Following resources can be injected using @ArquillianResource:

FacesContext

Application

ExternalContext

PartialViewContext

ELContext

ELResolver

ExpressionFactory

ViewHandler

NavigationHandler

ResourceHandler

ExceptionHandler

Flash

RenderKit

UIViewRoot

StateManager

4.9. Payload

Since an Inspection is a serializable object, it can contain a serializable payload which can be transferred with a request to a server, where it can be used as a base for a verification, for example:

  • contain an object which will be used to initialize UI

Similarly, an Inspection processed on a server is serialized and sent to a client, thus it can contain an arbitrary information which can be used as a base for client verification logic, for example:

  • provide a client with the locale information of a server

  • provide identifiers for UI elements in the generated page DOM

Following is an example of how one can use payload:

class CustomInspection {
  private SomePayload payload;
  ...
}

CustomInspection inspection = Warp
  .initiate(Activity)
  .inspect(new CustomInspection(payload));

SomePayload payload = inspection.getPayload();

4.10. Ability to Transfer an Inspection

The Inspection object has to follow just one contract in order to be transferable from a client to a server VM: all of its content has to be serializable.

Keep in mind that Inspection will be deployed to the server VM, so during its invocation, you can use only APIs available on server-side.

You can add desired APIs to the deployment and make them available to the server VM, but you should avoid exposing unwanted APIs to the server (e.g. API of client-side testing tools such as WebDriver).

The execution of an inspection may lead to ClassNotFoundException or LinkageError if you don’t stay compliant with this requirement.

4.10.1. Serializability of Anonymous/Inner Inspections

Anonymous and inner classes can be used as inspections due to transformation process which removes the binding from non-static inner classes to top-level classes.

Keep in mind:
  • you should not store a reference to a field in a top-level class

  • the state of an inspection is serialized and thus its mutations on a server are not directly exposed to the client

5. Debugging Warp

In order to debug Warp execution effectively, one needs to know few facts:

  • Warp tries to provide as much context during its failures as possible

    • (if you are not provided with a context of a failure, let’s report it)

  • the client-side execution can be debugged by usual approaches

  • the server-side execution can be debugged by usual approaches

  • Arquillian’s debug output can reveal issues

5.1. Arquillian Debugging Mode

In order to debug the tests effectively, one can reveal their internals by switching into the Arquillian debugging mode:

just pass -Darquillian.debug=true to the test and application server VM.

It will show an event tree in a console which exposes you:

  • what events are happening during test execution

  • what requests/responses were registered by Warp

  • what requests/responses were inspected by Warp

  • what lifecycle hooks were hit

5.2. Debug Warp and WebDriver

In order to debug Warp used together with WebDriver, one can use developer tools provided by browsers, such as:

  • Chrome Developer Tools

  • Firefox Developer Tools / Firebug

  • IE Developer Tools

  • Opera Dragonfly

and their ability to analyze Network traffic.

Refer to Graphene Reusable Session for best practices on how to develop with WebDriver. == Architecture

5.3. Warp Request Processing

In order to hook into client-to-server communication, Warp puts a HTTP proxy in between as illustrated on a image bellow.

This proxy observes requests incoming from a client and then enhances a request with a payload required for server inspection (processed referred to as "piggy-backing on a request").

Once an enhanced request enters a server, it is blocked by a request filter and an inspection is registered into an Arquillian system. The Warp’s filter then delegates the processing to the traditional request processing.

During request processing lifecycle, the Warp listens for appropriate lifecycle hooks, and as a response it can execute arbitrary actions which inspects the state of the request context.

To help with the full-featured verification, a Warp’s inspection process can leverage Arquillian’s dependency injection system.

Once the request is processed by the server, leading into committing response, Warp can collect the result of inspection and enhance a built response to the client (again using piggy-backing method).

The Warp’s proxy evaluates the response and either reports a failure (in case of server failure) or continues with execution of the test.

6. Learning From The Tests

In order to explore more use cases for Warp, the best way is to explore functional tests:

7. Further Reading

Issue Tracking

9. Community