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.
|
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.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:
|
executed before a servlet request enters servlet processing (in a servlet’s filter chain) |
|
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.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:
|
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: