RESTful testing with Webtest (incl. PUT, DELETE, HEAD, OPTIONS)

Dennis Knochenwefel <dennis.knochenwefel-Dn6QaV/[email protected]>
Newsgroups gmane.comp.web.canoo.webtest
Message-ID <[email protected]>
Hi all,

I've seen some demand for testing RESTful services (for example useful 
for testing AtomPub servers) with webtest. Currently, this cannot be 
done using webtest because "invoke" does not allow to change the content 
type for POST requests; Plus, HTTP method PUT cannot be used to send 
content.

Marc said earlier a patch would be welcome 
(http://lists.canoo.com/pipermail/webtest/2008q1/010021.html):
 > I don't think that you will be able to send a body currently with PUT 
for instance. Any patch is welcome.

So, I've attached a patch that solves this problem and makes webtest 
usable for any RESTful service (a door opener for a huge comunity). The 
patch should be 100% backwards compatible with hitherto functionality. 
It adds an additional attribute "contentType" which can be used to 
overwrite the content-type HTTP header. Also, I've enabled the PUT 
method to send body data within this patch.

I hope this is helpful for others as well and would be happy if it makes 
it into the trunk. Thank you for all your fantastic work.

cheers,

Dennis

-- 
Dennis Knochenwefel
Software Architect

28msec Inc.
http://www.28msec.com
http://twitter.com/28msec
restful.patch (text/plain, 5.8 KB)
Index: src/main/java/com/canoo/webtest/steps/request/InvokePage.java
===================================================================
--- src/main/java/com/canoo/webtest/steps/request/InvokePage.java	(Revision 117476)
+++ src/main/java/com/canoo/webtest/steps/request/InvokePage.java	(Arbeitskopie)
@@ -38,6 +38,7 @@
     private String fMethod = "GET";
     private File fContentFile;
     private String fContent;
+    private String fContentType;
 
     private String fSoapAction;
 
@@ -48,8 +49,25 @@
     public String getUrl() {
         return fUrl;
     }
+    
+    public String getContentType() {
+        return fContentType;
+    }
 
     /**
+     * Sets the HTTP Content Type.
+     *
+     * @param contentType
+     * @webtest.parameter
+     *   required="no"
+     *   description="Sets the Content type of the request. Defaults to 'application/x-www-form-urlencoded' for a POST request."
+     */
+    public void setContentType(final String contentType) {
+    	fContentType = contentType;
+    }
+    
+    /**
      * Alternative to set the content of a SOAP message.
      * @param txt the content
      * @webtest.nested.parameter
@@ -86,7 +104,7 @@
      * @webtest.parameter
      *   required="no"
      *   default="GET"
-     *   description="Sets the HTTP Method, i.e. whether the invoke is a GET or POST."
+     *   description="Sets the HTTP Method, i.e. whether the invoke is a GET or POST. Also, more advanced HTTP methods like PUT, DELETE, HEAD, or OPTIONS can be used."
      */
     public void setMethod(final String method) {
         fMethod = method;
@@ -143,40 +161,58 @@
     }
 
     protected void verifyParameters() {
-        super.verifyParameters();
-        nullParamCheck(getUrl(), "url");
-        paramCheck(getContent() != null && getContentFile() != null, "Only one of 'content' and 'contentFile' must be set.");
-        paramCheck("POST".equals(getMethod()) && getContent() == null && getContentFile() == null,
-                "One of 'content' or 'contentFile' must be set for POST.");
+    	super.verifyParameters();
+    	nullParamCheck(getUrl(), "url");
+        paramCheck(getContent() != null && getContentFile() != null, 
+        		"Only one of 'content' and 'contentFile' must be set.");
+    	
+        String method=getMethod().toUpperCase();
+        paramCheck(!"POST".equals(method) 
+        		&& !"GET".equals(method)
+        		&& !"PUT".equals(method)
+        		&& !"DELETE".equals(method)
+        		&& !"HEAD".equals(method)
+        		&& !"OPTIONS".equals(method),
+                "'method' must be one of GET, POST, PUT, DELETE, HEAD, or OPTIONS.");
+        paramCheck("POST".equals(method) && getContent() == null && getContentFile() == null,
+                "One of 'content' or 'contentFile' must be set for POST requests.");
+        paramCheck("PUT".equals(method) && getContent() == null && getContentFile() == null,
+                "One of 'content' or 'contentFile' must be set for PUT requests.");
+        paramCheck("GET".equals(method) && !(getContent() == null && getContentFile() == null),
+                "'content' and 'contentFile' must not be set for GET requests.");
+        paramCheck("DELETE".equals(method) && !(getContent() == null && getContentFile() == null),
+                "'content' and 'contentFile' must not be set for GET requests.");
+
     }
 
     protected Page findTarget() throws IOException, SAXException {
-        if ("POST".equals(getMethod())) {
-            return findTargetByPost();
+    	   fCompleteUrl = getContext().getConfig().getUrlForPage(getUrl());
+        final WebRequest request = new WebRequest(new URL(fCompleteUrl));
+        request.setHttpMethod(HttpMethod.valueOf(getMethod().toUpperCase()));
+        if ("POST".equals(getMethod().toUpperCase()) || "PUT".equals(getMethod().toUpperCase())) {
+            return findTargetByPostOrPut(request);
         }
-        fCompleteUrl = getContext().getConfig().getUrlForPage(getUrl());
-        final WebRequest settings = new WebRequest(new URL(fCompleteUrl));
-        settings.setHttpMethod(HttpMethod.valueOf(getMethod().toUpperCase()));
-        return getResponse(settings);
+        return getResponse(request);
     }
 
-    private Page findTargetByPost() throws IOException, SAXException {
-        String url = getContext().getConfig().getUrlForPage(getUrl());
-        final WebRequest settings = new WebRequest(new URL(url), HttpMethod.POST);
-        
-        // get default encoding
+    private Page findTargetByPostOrPut(WebRequest request) throws IOException, SAXException {
+    	// get default encoding
         final String charset = System.getProperty("file.encoding");
         
         final Map headers = new HashMap();
         if (!StringUtils.isEmpty(fSoapAction)) {
+        	
             headers.put("Content-type", "text/xml; charset=" + charset);
             headers.put("SOAPAction", fSoapAction);
-        } 
+        }
+        else if(!StringUtils.isEmpty(fContentType)) {
+            headers.put("Content-type", fContentType);
+        }
         else {
-            // TODO: is this the correct Content-type for non-SOAP posts?
             headers.put("Content-type", "application/x-www-form-urlencoded");
         }
-        settings.setAdditionalHeaders(headers);
+        
+        request.setAdditionalHeaders(headers);
         final String content;
         if (getContent() != null) {
             content = getContent();
@@ -184,9 +220,9 @@
         else {
             content = FileUtil.readFileToString(getContentFile(), this);
         }
-        settings.setRequestBody(content);
-        settings.setCharset(charset);
-        return getResponse(settings);
+        request.setRequestBody(content);
+        request.setCharset(charset);
+        return getResponse(request);
     }
 
     protected String getLogMessageForTarget() {
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.