It is pretty easy to simulate a web service using jetty. And with this example it will be even easier to create an integration test.
Note: This article was written assuming that you are already familiar with Maven, Log4j and JUnit.
The first thing we have to do is to add the Jetty dependency to our pom.xml file:
<dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-servlet-tester</artifactId> <version>6.1.16</version> <scope>test</scope> </dependency>
It is important that to set the scope of the dependency to test because we usually don't want to use this library in the production server.
The next step is to create a server class which it is going to receive the request and send back the response. This class is going to extend from the GenericServlet and we have to code only the service method. In the above example, the servlet gets the account number and looks in a map for the name of the file it has to return. Then it opens the file and copies its contents to the response.
By using a map and text files, it is very easy to add a new test case if that is required. The test files are kept in the src/test/resources directory.
import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; public class AccountServiceServlet extends GenericServlet { private static final Logger logger = Logger .getLogger(AccountServiceServlet.class); private static final long serialVersionUID = 1L; private Map<String, String> testAccountNumbers; public AccountServiceServlet() { this.getTestAccountNumbers().put("50386102", "/testFiles/50386102/AccountService_response01.xml"); this.getTestAccountNumbers().put("50166121", "/testFiles/50166121/AccountService_response.xml"); this.getTestAccountNumbers().put("00484119", "/testFiles/00484119/AccountService_response.xml"); } @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { BufferedReader contentReader = request.getReader(); StringBuilder stringBuilder = new StringBuilder(); String lineSep = System.getProperty("line.separator"); String nextLine; while ((nextLine = contentReader.readLine()) != null) { stringBuilder.append(nextLine); stringBuilder.append(lineSep); } String content = stringBuilder.toString(); logger.info("Received request: " + content); Pattern pattern = Pattern.compile("AccountNumber>(\\S+)</AccountNumber>"); Matcher aMatcher = pattern.matcher(content); if (aMatcher.find()) { String accountNumber = aMatcher.group(1); logger.debug("Getting response file for account #" + accountNumber); String testFileName = this.getTestAccountNumbers().get(accountNumber); logger.debug("Test File: " + testFileName); if (testFileName != null) { URL fileLocation = this.getClass().getResource(testFileName); if (fileLocation == null) throw new ServletException("Unable to find '" + testFileName + "'"); BufferedReader fileResponseReader; try { fileResponseReader = new BufferedReader(new FileReader(new File(fileLocation.toURI()))); } catch (URISyntaxException e) { throw new ServletException("Syntax error converting to an URI: " + fileLocation); } while ((nextLine = fileResponseReader.readLine()) != null) { response.getWriter().append(nextLine); response.getWriter().append(lineSep); } return; } } if(response instanceof HttpServletResponse){ HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); httpResponse.setContentType("text/xml"); httpResponse.setCharacterEncoding("UTF-8"); httpResponse.setContentLength(0); } else { throw new ServletException("The account doesn't exist."); } } public void setTestAccountNumbers(Map<String, String> testAccountNumbers) { this.testAccountNumbers = testAccountNumbers; } public Map<String, String> getTestAccountNumbers() { if (testAccountNumbers == null) testAccountNumbers = new HashMap<String, String>(); return testAccountNumbers; } }
Now we have to create a test which uses this servlet. There are a few methods we have to code here:
- initServletContainer: This method may have another name. It must create a new instance of the ServletTester and we must add one the class of our servlet and the path of the url. Then we get base url of the Jetty servlet tester and we assign it to a instance variable. The port of the servlet changes every time the test is run, so we have to get the base URL of the Jetty instance and assign it to the object which contains the configuration. In my case, I create an special mock configuration manager which allow me to set the URL on the fly.
import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Iterator; import junit.framework.Assert; import org.apache.log4j.Logger; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.mortbay.jetty.testing.ServletTester; public class TestAcccountServiceImplementation { private static final Logger logger = Logger .getLogger(TestAcccountServiceImplementation.class); private static ServletTester testServlet; private static final String SERVLET_ACCOUNT_SERVICE_PATH = "/QA/services/AccountService"; @BeforeClass public static void initServletContainer () throws Exception { testServlet = new ServletTester(); testServlet.setContextPath("/"); testServlet.addServlet(AccountServiceServlet.class, SERVLET_ACCOUNT_SERVICE_PATH); String servletBaseUrl = testServlet.createSocketConnector(false); String accountSeviceServletBaseUrl = servletBaseUrl + SERVLET_ACCOUNT_SERVICE_PATH; logger.info("Base URL of the account service servlet: " + accountSeviceServletBaseUrl); MockPropertiesConfiguration.setWebserviceAccountServiceBaseUrl(accountSeviceServletBaseUrl); testServlet.start(); }
- cleanupServletContainer: This method may have another name. This just stops the running instance of the Servlet Tester.
@AfterClass public static void cleanupServletContainer () throws Exception { testServlet.stop(); }
- testXXX: These methods are the ones which run the actual tests. In the above example there are two tests. The first one gets an account and validates the data returned by the account service. The second ones sets the URL of the web service to an incorrect port and test if the Web Service client throws an exception.
@Test public void testFindAccount() throws ProgramFault { AccountService accountService = Context.getAccoutService(); Account anAccount; AmericanAccount anAmericanAccount; logger.info("Testing account #50386102"); anAccount = accountService.getBrokerAccount("50386102"); Assert.assertNotNull(anAccount); Assert.assertTrue(anAccount instanceof CachedTermsAmericanAccount); anAmericanAccount = (AmericanAccount) anAccount; Assert.assertEquals("50386102", anAmericanAccount.getNumber()); Assert.assertEquals(3, anAmericanAccount.getTermsCount()); //More tests. logger.debug("End of find account test."); } @Test(expected=ProgramFault.class) public void testConnectionError() throws ProgramFault { logger.info("Testing connection error"); String oldDatabaseLookupUrl = MockPropertiesConfiguration.getWebserviceAccountServiceBaseUrl(); MockPropertiesConfiguration.setWebserviceAccountServiceBaseUrl("http://127.0.0.1:9999" + SERVLET_ACCOUNT_SERVICE_PATH); try { Context.getAccoutService().getCustomerAccount("999"); } finally { MockPropertiesConfiguration.setWebserviceDatabaseLookupBaseUrl(oldDatabaseLookupUrl); } } }
I changed a little bit the original code to reduce its size. In case you found any error, just let me know.