Discussion:
Running parallel unit tests under Jersey 1
cowwoc
2014-03-18 22:20:02 UTC
Permalink
Hi,

I just wanted to let you guys know that I managed to get run Jersey unit
tests in parallel using TestNG in "methods=parallel" mode, launching a
separate Jetty instance per @Test. My unit tests now complete 2x faster.
I don't know about you, but for me that is a very big deal.

My advice to you: dump JerseyTest. Its design is flawed and it doesn't
look like it'll get fixed anytime soon. You can replace Jetty with your
server of choice. Just dump the abstraction (JerseyTest) and talk
directly to the server.

Here is my code:

1. Unit tests extend "UnitTest" (below). It launches and shuts down a
separate server per @Test.
2. Each @Test accesses context-specific data through "TestClient". This
class holds a reference to the server URI and Jersey's Client instance.
3. The "JettyServer" class launches a Jetty instance against port 0
(meaning, it picks a random port number and ensures it is available
for use).

That's it. Easy as pie. I hope this helps other people.

-------------------
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.LoggingFilter;
import com.sun.jersey.api.json.JSONConfiguration;
import java.io.IOException;
import org.bitbucket.cowwoc.myapp.backend.JettyServer;
import org.bitbucket.cowwoc.myapp.backend.jackson.ObjectMapperProvider;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

/**
* Configures each @Test to run against a separate Jetty instance.
* <p>
* @author Gili Tzabari
*/
public abstract class UnitTest
{
private final ThreadLocal<JettyServer> jettyServer = new
ThreadLocal<>();
private final ThreadLocal<Client> jerseyClient = new ThreadLocal<>();

/**
* @return the context associated with the test
*/
protected TestClient getClient()
{
return new TestClient(jerseyClient.get(),
jettyServer.get().getBaseUri());
}

/**
* Launches a new server before each test.
* <p>
* @throws IOException if an I/O error occurs
*/
@BeforeMethod
protected void beforeTest() throws IOException
{
JettyServer server = new
JettyServer(TestContainer.class).setPort(0).start();

ClientConfig config = new DefaultClientConfig();
config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, true);
config.getClasses().add(ObjectMapperProvider.class);

Client client = Client.create(config);
client.addFilter(new LoggingFilter());

this.jettyServer.set(server);
this.jerseyClient.set(client);
}

/**
* Shuts down the server after each test.
* <p>
* @throws IOException if an I/O error occurs
*/
@AfterMethod
protected void afterTest() throws IOException
{
JettyServer server = this.jettyServer.get();
server.stop();
Client client = this.jerseyClient.get();
client.destroy();
this.jettyServer.remove();
this.jerseyClient.remove();
}
}
-------------------

-----------------------
import com.sun.jersey.spi.container.servlet.ServletContainer;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import org.bitbucket.cowwoc.preconditions.Preconditions;
import org.bitbucket.cowwoc.myapp.backend.jersey.MyApplication;
import org.bitbucket.cowwoc.myapp.backend.jersey.ServerMode;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.webapp.WebAppContext;

public final class JettyServer implements AutoCloseable
{
private final Server server = new Server();
private final Class<? extends ServletContainer> containerClass;
private int port = 80;
private String contextPath = "/";
private ServerMode mode = ServerMode.DEBUG;

/**
* Creates a new JettyServer.
* <p>
* @param containerClass the servlet or filter to respond to
incoming requests
* @throws NullPointerException if containerClass is null
*/
public JettyServer(Class<? extends ServletContainer> containerClass)
{
Preconditions.requireThat(containerClass,
"containerClass").isNotNull();
this.containerClass = containerClass;
}

/**
* Sets the port the server should listen on. The default value is
{@code 80}.
* <p>
* @param port 0 if a random port should be used
* @return this
*/
public JettyServer setPort(int port)
{
this.port = port;
return this;
}

/**
* @return the port the server should listen on
*/
public int getPort()
{
return port;
}

/**
* @return the port the server is listening on
* @throws IllegalStateException if the server is not running
*/
public int getBoundPort() throws IllegalStateException
{
// @see http://stackoverflow.com/a/14982831/14731
int result = ((NetworkConnector)
server.getConnectors()[0]).getLocalPort();
if (result < 0)
{
assert (!server.isStarted()): server;
throw new IllegalStateException("Server must be started");
}
return result;
}

/**
* @param contextPath the application context path. The default
value is {@code "/"}.
* @return this
* @throws NullPointerException if contextPath is null
*/
public JettyServer setContextPath(String contextPath)
{
Preconditions.requireThat(contextPath, "contextPath").isNotNull();
this.contextPath = contextPath;
return this;
}

/**
* @return the application context path
*/
public String getContextPath()
{
return contextPath;
}

/**
* @param mode the server mode. The default value is {@link
ServerMode#DEBUG}.
* @return this
* @throws NullPointerException if mode is null
*/
public JettyServer setServerMode(ServerMode mode)
{
Preconditions.requireThat(mode, "mode").isNotNull();
this.mode = mode;
return this;
}

/**
* @return the server mode
*/
public ServerMode getMode()
{
return mode;
}

/**
* Launches the server asynchronously.
* <p>
* @return this
* @throws IOException if an I/O error occurs
* @see #join()
*/
public JettyServer start() throws IOException
{
// Path to "target/classes"
Path moduleRoot = Modules.getRootPath(MyServer.class);
ResourceHandler sourceFiles = new ResourceHandler();
if (mode == ServerMode.DEBUG)
{
// Hotswap static resources during development
Path sourceDir =
moduleRoot.resolve("../../../frontend/src/main/webapp");
Preconditions.requireThat(sourceDir,
"sourceDir").isNotNull().exists();
sourceFiles.setResourceBase(sourceDir.toString());
}
Path webAppDir =
moduleRoot.resolve("../../../frontend/target/classes");
Preconditions.requireThat(webAppDir,
"webAppdir").isNotNull().exists();
ResourceHandler staticFiles = new ResourceHandler();
staticFiles.setResourceBase(webAppDir.toString());

HandlerList staticResourceHandlers = new HandlerList();
staticResourceHandlers.setHandlers(new Handler[]
{
sourceFiles, staticFiles
});

ContextHandler staticResources = new ContextHandler();
staticResources.setContextPath(contextPath);
staticResources.setHandler(staticResourceHandlers);

// Servlets and Javascript files loaded from META-INF/resources
in JAR files
WebAppContext dynamicResources = new WebAppContext();
dynamicResources.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed",
"false");
dynamicResources.setThrowUnavailableOnStartupException(true);
dynamicResources.setServer(server);
dynamicResources.setContextPath(contextPath);
// Avoid configuring system classpath because we've only got a
single webapp
dynamicResources.setParentLoaderPriority(true);
dynamicResources.setResourceBase(moduleRoot.toString());
FilterHolder container =
dynamicResources.addFilter(containerClass, "/*",
EnumSet.allOf(DispatcherType.class));
container.setInitParameter("mode", mode.name());
container.setInitParameter("javax.ws.rs.Application",
MyApplication.class.getName());

HandlerList topLevelHandlers = new HandlerList();
topLevelHandlers.setHandlers(new Handler[]
{
staticResources, dynamicResources
});

ServerConnector httpConnector = new ServerConnector(server);
httpConnector.setPort(port);
server.addConnector(httpConnector);
server.setHandler(topLevelHandlers);

server.setStopAtShutdown(true);
try
{
server.start();
}
catch (IOException | AssertionError e)
{
stop();
throw e;
}
catch (Exception e)
{
stop();
throw new IOException(e);
}
return this;
}

/**
* Blocks until the server finishes launching.
* <p>
* @return this
* @throws InterruptedException if the thread is interrupted while
waiting
*/
public JettyServer join() throws InterruptedException
{
server.join();
return this;
}

/**
* Shuts down the server.
* <p>
* @return this
* @throws IOException if an I/O error occurs
*/
public JettyServer stop() throws IOException
{
try
{
server.stop();
}
catch (Exception e)
{
IOException ioException = new IOException(e);
ioException.addSuppressed(e);
throw ioException;
}
return this;
}

/**
* @return the base URI of the application
*/
public URI getBaseUri()
{
return server.getURI();
}

@Override
public void close() throws IOException
{
stop();
}
}
---------------

------------------------------
/**
* The context associated with a @Test.
* <p>
* @author Gili Tzabari
*/
public class TestClient
{
private final Client delegate;
private final URI baseUri;

/**
* Creates a new TestClient.
* <p>
* @param client the Jersey client to delegate to
* @param baseUri the base URI of the application
* @throws NullPointerException if client or baseUri are null
*/
public TestClient(Client client, URI baseUri)
{
Preconditions.requireThat(client, "client").isNotNull();
Preconditions.requireThat(baseUri, "baseUri").isNotNull();
this.delegate = client;
this.baseUri = baseUri;
}

/**
* @return a web resource whose URI refers to the base URI of the
application
*/
public WebResource resource()
{
return delegate.resource(baseUri);
}

// Convenience methods that facilitate construction of common
client-side classes

/**
* @return the companies resource
*/
public CompaniesResource companies()
{
return new CompaniesResource(resource().path("companies/"));
}
}
--------------

Gili
Vetle Leinonen-Roeim
2014-03-19 07:30:48 UTC
Permalink
Post by cowwoc
Hi,
I just wanted to let you guys know that I managed to get run Jersey unit
tests in parallel using TestNG in "methods=parallel" mode, launching a
I don't know about you, but for me that is a very big deal.
Awesome!
Post by cowwoc
My advice to you: dump JerseyTest. Its design is flawed and it doesn't
look like it'll get fixed anytime soon. You can replace Jetty with your
server of choice. Just dump the abstraction (JerseyTest) and talk
directly to the server.
Have you tried Jersey 2? As long as you don't need the servlet API, you
can use an in-memory container when running JerseyTest, and blast away.
It's really neat.

[...]

Regards,
Vetle
Joe Mocker
2014-03-19 15:16:08 UTC
Permalink
I had written a ContainerFactory for JerseyTest to configure an in-memory Jetty instance a while ago to work around some broken pieces of JerseyTest too,

https://github.com/creechy/jetty-jerseytest-container

I like the idea of punting JerseyTest, too, now that I look back at it, there’s not a heck of a lot of value-add compared to all its issues.

—joe
Post by Vetle Leinonen-Roeim
Post by cowwoc
Hi,
I just wanted to let you guys know that I managed to get run Jersey unit
tests in parallel using TestNG in "methods=parallel" mode, launching a
I don't know about you, but for me that is a very big deal.
Awesome!
Post by cowwoc
My advice to you: dump JerseyTest. Its design is flawed and it doesn't
look like it'll get fixed anytime soon. You can replace Jetty with your
server of choice. Just dump the abstraction (JerseyTest) and talk
directly to the server.
Have you tried Jersey 2? As long as you don't need the servlet API, you can use an in-memory container when running JerseyTest, and blast away. It's really neat.
[...]
Regards,
Vetle
cowwoc
2014-03-19 18:09:09 UTC
Permalink
Post by Vetle Leinonen-Roeim
Post by cowwoc
My advice to you: dump JerseyTest. Its design is flawed and it doesn't
look like it'll get fixed anytime soon. You can replace Jetty with your
server of choice. Just dump the abstraction (JerseyTest) and talk
directly to the server.
Have you tried Jersey 2? As long as you don't need the servlet API,
you can use an in-memory container when running JerseyTest, and blast
away. It's really neat.
I don't plan to upgrade to Jersey 2 until the critical features missing
from Jersey 1 are fully ported over. The major one that comes to mind is
ResourceContext: http://stackoverflow.com/q/17284419/14731.

As for running an in-memory container:
http://stackoverflow.com/q/13283542/14731

Meaning, I don't understand (and I genuinely want someone to explain to
me) what an in-memory container gives you that a normal JUnit test
(without a server) does not.

So to answer your question, all my unit tests assume the existence of
the Servlet API so the above is the best I can do.

Gili
Vetle Leinonen-Roeim
2014-03-19 20:09:04 UTC
Permalink
Post by cowwoc
Post by Vetle Leinonen-Roeim
Post by cowwoc
My advice to you: dump JerseyTest. Its design is flawed and it doesn't
look like it'll get fixed anytime soon. You can replace Jetty with your
server of choice. Just dump the abstraction (JerseyTest) and talk
directly to the server.
Have you tried Jersey 2? As long as you don't need the servlet API,
you can use an in-memory container when running JerseyTest, and blast
away. It's really neat.
I don't plan to upgrade to Jersey 2 until the critical features missing
from Jersey 1 are fully ported over. The major one that comes to mind is
ResourceContext: http://stackoverflow.com/q/17284419/14731.
http://stackoverflow.com/q/13283542/14731
Meaning, I don't understand (and I genuinely want someone to explain to
me) what an in-memory container gives you that a normal JUnit test
(without a server) does not.
For me it helps with the integration tests. I can easily deploy the
whole application, run integration tests in parallel if I want to, on a
shared build server.

I suppose using JerseyTest you can make sure your writers and readers
are functioning properly. For instance, you can of course write a unit
test for a writer, but if you use JerseyTest you can add a resource that
expects the reader or writer to work properly with whatever it consumes
or produces. So in this way it enables you to test that setup is correct.
Post by cowwoc
So to answer your question, all my unit tests assume the existence of
the Servlet API so the above is the best I can do.
Yeah, it seems that the preferred route in Jersey 2 is to not depend on
the servlet API at all. If that can be accomplished, then I really like
the flexibility Jersey 2 provides wrt using the different containers
(not just for tests).

When it comes to integration tests - again - if you're not depending on
the servlet API. then it's just a matter of setting it up with your
ResourceConfig, and target("whatever").request() ... and so on. Simple!

Regards,
Vetle

Loading...