Filed under: Cucumber, Maven, Test automation, — Tags: BDD, Behaviour Driven Development, Behaviour Driven Development - BDD, Cucumber, Cucumber-jvm, Executable specifications, Java, POJO, Readability, SoapUI, Test web services, soap — Thomas Sundberg — 2011-11-16
An example is perhaps the best way to describe something. Concrete examples are easier to understand than abstract descriptions.
I will show how SoapUI can be used to test a web service. I will also show three different tools that can be used to control SoapUI. This tool chain can easily be made a part of your continuous build.
The example I will use is about car maintenance. A car with an empty fuel tank need to be refueled. The car exists behind a web service with three methods defined using the Web Service Definition Language, WSDL, below.
<definitions xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://example.sigma.se/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://example.sigma.se/" name="CarService"> <types> <xsd:schema> <xsd:import namespace="http://example.sigma.se/" schemaLocation="http://localhost:8090/car?xsd=1"/> </xsd:schema> </types> <message name="addFuel"> <part name="parameters" element="tns:addFuel"/> </message> <message name="addFuelResponse"> <part name="parameters" element="tns:addFuelResponse"/> </message> <message name="getFuelLevel"> <part name="parameters" element="tns:getFuelLevel"/> </message> <message name="getFuelLevelResponse"> <part name="parameters" element="tns:getFuelLevelResponse"/> </message> <message name="emptyFuel"> <part name="parameters" element="tns:emptyFuel"/> </message> <message name="emptyFuelResponse"> <part name="parameters" element="tns:emptyFuelResponse"/> </message> <portType name="Car"> <operation name="addFuel"> <input wsam:Action="http://example.sigma.se/Car/addFuelRequest" message="tns:addFuel"/> <output wsam:Action="http://example.sigma.se/Car/addFuelResponse" message="tns:addFuelResponse"/> </operation> <operation name="getFuelLevel"> <input wsam:Action="http://example.sigma.se/Car/getFuelLevelRequest" message="tns:getFuelLevel"/> <output wsam:Action="http://example.sigma.se/Car/getFuelLevelResponse" message="tns:getFuelLevelResponse"/> </operation> <operation name="emptyFuel"> <input wsam:Action="http://example.sigma.se/Car/emptyFuelRequest" message="tns:emptyFuel"/> <output wsam:Action="http://example.sigma.se/Car/emptyFuelResponse" message="tns:emptyFuelResponse"/> </operation> </portType> <binding name="CarPortBinding" type="tns:Car"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/> <operation name="addFuel"> <soap:operation soapAction=""/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation> <operation name="getFuelLevel"> <soap:operation soapAction=""/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation> <operation name="emptyFuel"> <soap:operation soapAction=""/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation> </binding> <service name="CarService"> <port name="CarPort" binding="tns:CarPortBinding"> <soap:address location="http://localhost:8090/car"/> </port> </service> </definitions>
We can see that there are three methods available here
This a really boring example, you can add fuel, check the level and empty the tank. But it is sufficient complicated so you can get a feeling that the example actually does something. We will not use the WSDL, it is presented just so you can see the definition and be glad that you don't have to penetrate everything to understand the example.
Before you start building the example, I need to show the file structure this example lives in.
example --- product --- src -- main -- java -- se -- sigma -- example --- Car.java | | | | | -- WebService.java | | | | | -- pom.xml | | -- test --- src -- test -- java -- se -- sigma -- example --- FuelCarTest.java | | | | | | | -- FuelCarSteps.java | | | | | -- resources -- se -- sigma -- example -- CarMaintenance.feature | | | -- pom.xml | | -- pom.xml
Given this file structure, you should be able to re-create this example.
We have seen the WSDL for the web service that will be used in the example. The actual implementation is done using two classes, Car and WebService. Car is the domain logic and WebService is the provider that will make Car available.
The Car implementation:
File: product/src/main/java/se/sigma/example/Car.java package se.sigma.example; import javax.jws.WebMethod; import javax.jws.WebService; import java.util.Date; @WebService public class Car { private Integer fuelLevel; public Car() { fuelLevel = 0; } @WebMethod public void addFuel(int addedAmount) { String message = "adding " + addedAmount; usageLog(message); fuelLevel = fuelLevel + addedAmount; } @WebMethod public Integer getFuelLevel() { String message = "returning fuel level " + fuelLevel; usageLog(message); return fuelLevel; } @WebMethod public void emptyFuel() { String message = "Emptying fuel tank"; usageLog(message); fuelLevel = 0; } private void usageLog(String message) { Date now = new Date(); System.out.println(now + " " + message); } }
This is nothing more than a pojo, plain old java object, with some annotations.
The simplest possible thing that could work for providing this as a web service might be to use the javax.xml.ws.Endpoint class as the publishing tool. It will take the annotated class make it available as a web service. The implementation I use looks like this:
File: product/src/main/java/se/sigma/example/WebService.java package se.sigma.example; import javax.xml.ws.Endpoint; public class WebService { public static void main(String[] args) { Endpoint.publish("http://localhost:8090/car", new Car()); } }
The service will be published behind port 8090 and the context car. You should be able to access it from http://localhost:8090/car
The final part that is needed to tie this example together is a Maven pom. The one I used here looks like:
File: product/pom.xml <?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.sigma.cucumber</groupId> <artifactId>example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>product</artifactId> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>com.sun.xml.ws</groupId> <artifactId>jaxws-rt</artifactId> <version>2.2.5</version> </dependency> </dependencies> </project>
This simple implementation created the WSDL above. The next step is to create a SoapUI project that can be used to explore and test the service.
The rest of this example assumes that the main defined above in the WebService is running. Start it from your IDE or whatever tool you use to edit Java. I started it from the tool of my choice, IntelliJ IDEA.
The star in this example is SoapUI. SoapUI is an open source testing tool that will allow you to explore a Web Service just by examining the WSDL. An introduction with some images can be found at http://www.soapui.org I will limit my self to explain the steps brief in text.
Now we have a working SoapUI project. We could stay here. But that would mean that somebody would have to run the test manually. We would rather have a build system or similar to run the test often. Lets use the SoapUI project and connect to three different tools. These tools are:
Maven is a build tool that many people has opinions about, either they hate it or they love it. I will show how the Maven SoapUI plugin can be configured to run the project we just set up.
We need to define a plugin repository, SoapUI isn't available on Maven Central. It should be http://www.eviware.com/repository/maven2/
<pluginRepositories> <pluginRepository> <id>eviwarePluginRepository</id> <url>http://www.eviware.com/repository/maven2/</url> </pluginRepository> </pluginRepositories>
The plugin also has to be configured
<plugin> <groupId>eviware</groupId> <artifactId>maven-soapui-plugin</artifactId> <version>4.0.1</version> <configuration> <projectFile>test/src/test/soapUI/CarMaintenance-soapui-project.xml</projectFile> <outputFolder>./test/target/soapUI</outputFolder> <junitReport>true</junitReport> <printReport>true</printReport> <projectProperties> <value>addedFuel=17</value> <value>expectedFuel=17</value> </projectProperties> </configuration> <executions> <execution> <phase>integration-test</phase> <goals> <goal>test</goal> </goals> </execution> </executions> </plugin>
The most important things here are of course the parameters that we may want to vary and the path to the SoapUI script.
Note the syntax for defining the parameters, you have to have the name followed by an equal sign and than the value. There may not be any spaces.
The complete Maven pom will be available below.
Another option for testing web services through SoapUI is to connect to it from JUnit. This would eliminate the usage of the Maven Plugin. An example is the JUnit implementation below:
File: test/src/test/java/se/sigma/example/junit/FuelCarTest.java package se.sigma.example.junit; import com.eviware.soapui.tools.SoapUITestCaseRunner; import org.junit.Test; public class FuelCarTest { @Test public void verifyTheInputValueIsReturned() throws Exception { SoapUITestCaseRunner runner = new SoapUITestCaseRunner(); runner.setProjectFile("/Users/tsu/Dropbox/projects/tsu/blog/soapUI-junit-maven-cucumber/example/test/src/test/soapUI/CarMaintenance-soapui-project.xml"); String[] properties = new String[2]; properties[0] = "addedFuel=42"; properties[1] = "expectedFuel=42"; runner.setProjectProperties(properties); runner.run(); } }
The parameters are set using the same syntax as in the Maven plugin. They are defined in the array properties and passed to the script. The path to the SoapUI script is defined as an absolute path, a relative path resulted in problems for the SoapUITestCaseRunner to locate the script.
Both the Maven plugin and the JUnit implementation has problems with it descriptions. It is not easy to look at the Maven plugin and decide what this test actually does. Similar, it is not very easy to look at the JUnit implementation and see what actually is going on here. This may be corrected with the next tool, Cucumber.
Cucumber is a Behaviour Driven Development, BDD, tool that allows you to define what you expect with the syntax
This example is than translated to executable code through some step definitions. The steps are not meant for somebody unfamiliar with code to read. They should read a feature that defines what we expect, other has to implement and read the steps that actually uses the system under test.
A feature that defines what we want to verify may look like:
File: test/src/test/resources/se.sigma.example.cucumber/CarMaintenance.feature Feature: Daily car maintenance Cars need maintenance Scenario: Fuelling Given a car with an empty gas tank When you fill it with 50 litres of fuel Then the tank contains 50 litres
This feature cannot live by itself, it need support. The most important thing is to define the steps that actually connects to the Web Service. One implementation may look like:
File: test/src/test/java/se/sigma/example/cucumber/FuelCarSteps.java package se.sigma.example.cucumber; import com.eviware.soapui.tools.SoapUITestCaseRunner; import cucumber.annotation.en.Given; import cucumber.annotation.en.Then; import cucumber.annotation.en.When; public class FuelCarSteps { private String[] properties = new String[2]; @Given("^a car with an empty gas tank$") public void a_car_with_an_empty_gas_tank() { // Nothing to do here, it will be taken care of in the SoapUI script } @When("^you fill it with (.*) litres of fuel$") public void you_fill_it_with_litres_of_fuel(String addedFuel) { properties[0] = "addedFuel=" + addedFuel; } @Then("^the tank contains (.*) litres$") public void the_tank_contains_litres(String expectedFuel) throws Exception { properties[1] = "expectedFuel=" + expectedFuel; SoapUITestCaseRunner runner = new SoapUITestCaseRunner(); runner.setProjectFile("/Users/tsu/Dropbox/projects/tsu/blog/soapUI-junit-maven-cucumber/example/test/src/test/soapUI/CarMaintenance-soapui-project.xml"); runner.setProjectProperties(properties); runner.run(); } }
The Given and When steps doesn't really do anything else than read some parameters and store them so they are available in the final Then step. It is of course the Then step that actually performs anything.
The steps need to be glued together with the steps. It can be done using a JUnit runner. It is implemented as:
File: test/src/test/java/se/sigma/example/cucumber/FuelCarTest.java package se.sigma.example.cucumber; import cucumber.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class FuelCarTest { }
Note that the steps may not be defined in the test class. Steps are defined globally and defining them in the test class would tie them hard to this particular feature.
A Maven pom that supports all three tools at the same time may look like this:
File: test/pom.xml <?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.sigma.cucumber</groupId> <artifactId>example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>test</artifactId> <repositories> <repository> <id>soapUI</id> <url>http://www.eviware.com/repository/maven2/</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>eviwarePluginRepository</id> <url>http://www.eviware.com/repository/maven2/</url> </pluginRepository> </pluginRepositories> <build> <plugins> <plugin> <groupId>eviware</groupId> <artifactId>maven-soapui-plugin</artifactId> <version>4.0.1</version> <configuration> <projectFile>./example/test/src/test/soapUI/CarMaintenance-soapui-project.xml</projectFile> <outputFolder>./test/target/soapUI</outputFolder> <junitReport>true</junitReport> <printReport>true</printReport> <projectProperties> <value>addedFuel=17</value> <value>expectedFuel=17</value> </projectProperties> </configuration> <executions> <execution> <phase>integration-test</phase> <goals> <goal>test</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>se.sigma.cucumber</groupId> <artifactId>product</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> </dependency> <dependency> <groupId>eviware</groupId> <artifactId>maven-soapui-plugin</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.0.1</version> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.0.1</version> </dependency> </dependencies> </project>
It is not very difficult to verify a web service. We need some tools and we need to connect them properly. I choosed to use Cucumber to increase the readability in this example. If you prefer using the Maven plugin or JUnit, please do so.
This post has been reviewed by some people who I wish to thank for their help
Thank you very much for your feedback!