Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Running unit tests on the server (JAX-RS)

Tags:

I am writing a JAX-RS (Jersey+Maven) application that does some tricky things (eg call native executables embedded in the WAR). I need to run [some of] my unit tests (JUnit4) on the server (Amazon Elastic Beanstalk running Tomcat 7.0.22) to check that everything is ok.

Is there a standard, flexible way of doing this other than RYO (roll your own)? The things I found seem to have more to do with integration testing on the developer machine (ie, Jersey Test Framework). Even RYO is confusing me... how could I call code in the Test Packages from Source Packages?

Basically, I want to create a /test resource that I can call that will return my unit test results from the server in a pretty format. Even better if I could do /test/{category}

like image 758
Aleksandr Dubinsky Avatar asked Jan 16 '12 09:01

Aleksandr Dubinsky


People also ask

Can you unit test a REST API?

Unit tests are just one of many software testing methods for testing your REST APIs. They should be used wherever it is appropriate.


1 Answers

I wanted to share what I've learned after posting this question and put up my first answer on StackExchange (a site at which I've arrived countless times through google in search of solutions to my endless problems)

The unit vs integration vs functional testing continuum

There a lot of correcting and arguing and trolling on this subject, so I'd like to clear it up. It's all really very simple. Say you have some service. When you call it there is a chain of events that I'll simplistically illustrate as:

(request received) - (function 1 called) - (function 2 called) - (function 3 called) - (response sent)

Unit testing tests each function (or class or unit) individually in isolation, feeding in an input and checking the output. Integration testing takes several units (such as the function 2-function 3 chain) and also does the ol' in-and-out. Functional testing runs through the entire chain, from request to response. I'll leave it to the reader to guess at some advantages and disadvantages of testing at each level of scale. Anyway, ALL OF THESE TESTS CAN BE RUN IN THE SERVER, AND THERE ARE GOOD REASONS TO RUN THEM THERE.

Types of in-container/in-server testing

  • Container-in-the-tests A feature of Spring and other dependency injection frameworks lets you set up a container that's filled only with the bare minimum classes (plus all the mocks) for each of your testss. This is very convenient, since it removes the need for manual wiring and better approximates the production environment. This only allows unit and integration testing.
    • Advantages: a) traditional unit testing (with its advantages of focused and isolated tests) made more convenient b) closer to the production environment since you're testing the autowiring logic e) integrates with IDE test runner f) quick
    • Disadvantages: a) the environment can be rather different from production b) doesn't replace the need for functional testing
  • Server-in-the-tests An ordinary test runner runs almost-ordinary unit tests which start up an embedded server or container, and make calls to it. A few frameworks (like Jersey Testing Framework) only allow functional testing, but most (Arquillian, jeeunit) let you do all types. With some of these frameworks, it's as if the tests are running on the server alonside your code and can make any sorts of calls.
    • Advantages (besides the fact you have access to all the container and server services): a) you have self-contained tests and don't need to install or set up anything b) the tests are isolated because a fresh server/container is created for each test or test suite. b) integrates with IDE test runner
    • Disadvantages: a) the environment can be rather different from production (eg, Jetty isn't Tomcat or Glassfish) b) starting/stoping the server slows down the tests c) the frameworks suck. Jeeunit is a tiny project that hasn't even been tested on Windows, Arquillian is big but very new, poorly documented, and I couldn't get it to work either.
  • Tests-in-the-server Here, the tests actually are compiled with and run alongside your code.
    • Advantages: a) you have plain, old tests that don't need to be aware of or use any sort of framework
    • Disadvantages: a) no isolation between tests (not necessarily a problem, or even a disadvantage, but may have to take precautions) b) doesn't integrate with IDE test runner (at least in Netbeans)
    • Using Maven during build Maven starts up a server, loads in your special test WAR, executes the tests, and gives a nice Surefire report.
      • Additional advantages: a) it'd done during build (and will integrate with Continuous Integration tools and others) b) no need to install or set up anything (Maven will download, run, etc the server automatically)
      • Additional disadvantages: a) the environment can be rather different (Maven uses Jetty, and it runs on your machine) b) can't re-run in production
    • in-WAR testing Tests are permanently compiled with your code. Whenever and wherever your WAR is up, you can fire up the tests. On your development server, during staging, even in production. This is what my original question was.
      • Additional advantages: a) EXACTLY the correct environment. b) run tests whenever
      • Additional disadvantages: a) need to set up a server

There's one more point to make. Netbeans gives most of the benefits of Maven testing to in-WAR testing. It includes an embedded server, and starts and deploys to it automatically after build. It even open up Firefox... just set it up to point to your /test resource. It's just like doing it the Maven way, but better.

Anyway, I'll show you how to do Maven testing and in-WAR testing together in the same Maven project.

Container-in-the-tests using Spring:

Spring is a sprawling container framework. Its dependency injection mechanisms intertwine with Jax-RS to glorious effect, at the cost of a significant learning curve. I won't explain how Spring or Jax-RS works. I'll jump right into the instructions and hopefully readers can adapt the ideas to other scenarios.

The way to get a container going in your JUnit 4 tests is to use the Spring test runner, declare the classes you'd like to register in the container, register some Jax-RS-specific helper classes, register your mocks, and finally use your Jax-RS resource as if it were an ordinary class:

@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes={     MyClass1.class,     Myclass2.class,     MyJaxRsResource.class,     MockServletContextAwareProcessor.class,     MyCTest.Config.class }) public class MyCTest {     @Configuration     static class Config      {           // Set up and register mocks here, and watch them be autowired!           @Bean public DBService dbJobService() throws DBException             {                 return mock(DBService.class);              }     }      @Autowired MyJaxRsResource myResource;      @Test public void test() {          String response = myResource.get("hello");     } } 

@WebAppConfiguration injects its own ServletContextAwareProcessor. However, MockServletContextAwareProcessor is necessary when the path to the unpacked WAR file has to be set dynamically, since WebAppConfiguration only lets you set the path statically at compile time. Using this class when running the-tests-in-the-server (see below), I inject the real ServletContext. I used Spring's profiles feature to suppress it via an environment variable (which isn't very elegant). setServletContext is called simply by the server test runner.

@Configuration public class MockServletContextAwareProcessor {  public static void setServletContext(ServletContext sc) {     servletContext = sc; }     private static ServletContext getServletContext() {     return servletContext; } private static ServletContext servletContext;          @Configuration @Profile("server-test") static class ServerTestContext {      static public @Bean     ServletContextAwareProcessor          scap() {             ServletContext sc = getServletContext();             return new ServletContextAwareProcessor(sc);     } }     } 

Server-in-the-tests using Maven:

Step 1) Create regular JUnit tests in the /src/test folder, but name them IT*.java or *IT.java or *ITCase.java (eg, MyClassIT.java) You can name them differently, but this is what Failsafe expects by default. IT stands for integration test, but the test code can lie anywhere on the testing continuum. Eg, you can instantiate a class and unit test it, or you can fire up HttpClient (or Jersey Client), point it at yourself (note the port below), and functionally test your entrypoints.

public class CrossdomainPolicyResourceSTest extends BaseTestClass {  static com.sun.jersey.api.client.Client client;    @BeforeClass public static void  startClient() {          client = Client.create();     }    @Test public void  getPolicy() {          String response =              client                 .resource("http://localhost/crossdomain.xml")                 .get(String.class);          assertTrue(response.startsWith("<?xml version=\"1.0\"?>"));     } } 

BaseTestClass is just a little helper class that prints the name of the test class and test as it executes (useful for tests-in-server, see below):

public abstract class BaseTestClass {  @ClassRule public static TestClassName className = new TestClassName(); @Rule public TestName testName = new TestName();        @BeforeClass public static void  printClassName() {          System.out.println("--" + className.getClassName() + "--");      }       @Before public void  printMethodName() {         System.out.print(" " + testName.getMethodName());      }       @After public void  printNewLine() {          System.out.println();      } } 

Step 2) Add maven-failsafe-plugin and maven-jetty-plugin to your pom.xml

<plugin>     <groupId>org.apache.maven.plugins</groupId>     <artifactId>maven-failsafe-plugin</artifactId>     <version>2.11</version>     <executions>         <execution>             <goals>                 <goal>integration-test</goal>                 <goal>verify</goal>             </goals>         </execution>     </executions> </plugin> <plugin>     <groupId>org.mortbay.jetty</groupId>     <artifactId>maven-jetty-plugin</artifactId>     <version>6.1.26</version>     <configuration>         <!-- By default the artifactId is taken, override it with something simple -->         <contextPath>/</contextPath>         <scanIntervalSeconds>2</scanIntervalSeconds>         <stopKey>foo</stopKey>         <stopPort>9999</stopPort>         <connectors>             <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">                 <port>9095</port>                 <maxIdleTime>60000</maxIdleTime>             </connector>         </connectors>     </configuration>     <executions>         <execution>             <id>start-jetty</id>             <phase>pre-integration-test</phase>             <goals>                 <goal>run</goal>             </goals>             <configuration>                 <scanIntervalSeconds>0</scanIntervalSeconds>                 <daemon>true</daemon>             </configuration>         </execution>         <execution>             <id>stop-jetty</id>             <phase>post-integration-test</phase>             <goals>                 <goal>stop</goal>             </goals>         </execution>     </executions> </plugin> 

Step 3) Profit. Really, that's it! Just run 'mvn install' or hit build in the IDE, and the code will build, your regular *Test.java tests will run, the jetty server will start up, the *IT.java tests will run, and you'll get a nice report.

Packaging your tests in your WAR to run anywhere:

(use together or separately from above instructions)

Step 1) Get your test classes (the src/test/ directory) embedded in the WAR by instructing the maven-war-plugin to include them: (adapted from here)

<plugin>     <groupId>org.apache.maven.plugins</groupId>     <artifactId>maven-war-plugin</artifactId>     <version>2.1.1</version>     <configuration>         <failOnMissingWebXml>false</failOnMissingWebXml>         <webResources>             <resource>                 <directory>${project.build.directory}/test-classes</directory>                 <targetPath>WEB-INF/classes</targetPath>             </resource>             <resource>                 <directory>${project.build.directory}/test-libs</directory>                 <targetPath>WEB-INF/lib</targetPath>             </resource>         </webResources>     </configuration> </plugin> 

Note: You can create a separate WAR with integrated tests by creating an additional execution and in its configuration set and (the details I leave to the reader)

Note: Ideally, the above would exclude all regular tests (and only copy *IT.java) However, I couldn't get includes/excludes to work.

You will also have to include the test libraries by giving the maven-dependency-plugin an additional execution with a goal of copy-dependency that includes the test scope

<plugin>     <groupId>org.apache.maven.plugins</groupId>     <artifactId>maven-dependency-plugin</artifactId>     <version>2.1</version>     <executions>         <execution>             <id>copy-dependencies</id>             <phase>prepare-package</phase>             <goals>                 <goal>copy-dependencies</goal>             </goals>             <configuration>                 <excludeScope>compile</excludeScope>                 <outputDirectory>${project.build.directory}/test-libs</outputDirectory>                 <overWriteReleases>true</overWriteReleases>                 <overWriteSnapshots>true</overWriteSnapshots>                 <overWriteIfNewer>true</overWriteIfNewer>             </configuration>         </execution>     </executions> </plugin> 

If maven-dependency-plugin already has other executions (eg, Netbeans inserts one for javaee-endorsed-api), do not delete them.

Step 2) Programmatically run your tests using JUnitCore (JUnit4).

String runTests() {     PrintStream sysOut = System.out;     PrintStream sysErr = System.err;     ByteArrayOutputStream stream = new ByteArrayOutputStream();     PrintStream out = new PrintStream(stream);     try {         System.setOut(out);         System.setErr(out);         TextListener listener = new TextListener(out);         JUnitCore junit = new JUnitCore();         junit.addListener(listener);                  junit.run(MyClassIT.class,                   AnotherClassIT.class,                   ...etc...);      } finally {         System.setOut(sysOut);         System.setErr(sysErr);         out.close();     }          return stream.toString(); } 

Step 3) Expose your tests via JAX-RS

@Path("/test") public class TestResource {      @GET     @Produces("text/plain")     public String getTestResults() {            return runTests();     }      private String runTests() {         ...     }  } 

Put this class along with your other test classes (in src/test) so that it could reference them.

However, if you are subclassing the javax.ws.rs.core.Application class where you're registering all your resources, you'll have a problem referencing TestResource (since source code can't reference test code). To work around this, create a completely empty dummy TestResource class under src/main/...[same package]... This trick works because the dummy TestResource will be overwritten by the real one during packaging.

public class ShoppingApplication extends Application {      @Override     public Set<Class<?>> getClasses() {         return new HashSet<Class<?>>() {{             add(TestResource.class);         }};     }      @Override     public Set<Object> getSingletons() {         return new HashSet<Object>();     } }  package ...same package as the real TestResource... public class TestResource {  } 

Step 4) Set up your IDE to launch/deploy your app and open your browser point to "/test" automatically after build.

like image 67
Aleksandr Dubinsky Avatar answered Oct 11 '22 05:10

Aleksandr Dubinsky