Unit testing
Automated testing is an extremely useful tool to find and prevent bugs and guide the overall development of your application.
Testing your code is always a best practice and the zero.test module provides an easy way to execute your tests. Test can help eliminate bugs early in the development process and can be used to solve a number of problems such as:
- Writing tests as you introduce new functionality validates that your code works as expected.
- Modifying existing code allows tests to verify that changes do not affect the behavior of your code.
The zero.test module leverages the JUnit
4 testing framework. JUnit is well-known and utilizes an easy to learn pattern.
There are many tutorials available through a web search that illustrate how to
get started with writing JUnit tests. This article focuses on how to invoke
JUnit tests in Groovy and Java and some extensions sMash provides to ease application
testing.
Because Web applications are layered architectures they can often be complex to test. The zero.test provides some conveniences and utilities to help and provides an test execution environment that helps you verify that your code is doing what it should be doing.
The following is divided in to three sections:
Getting zero.test
To begin using zero.test you first add it as a dependency in your
application. IBM® WebSphere® sMash's dependency manager will find the
necessary dependencies and add them to your application's classpath.
After creating your application, modify the config/ivy.xml file in your
application by adding the following line in the dependencies section:
<dependency org="zero" name="zero.test" rev="[1.0.0.0, 2.0.0.0[" />
After adding the dependency, issue the resolve command using the CLI. For instance, from the command line:
zero resolve
Running tests
Tests are run by executing the zero test CLI task:
zero test
By default, this will run every JUnit test in the current application where the
class ends with Test. For instance,my.custom.UnitTest
would be executed but my.custom.TestingClass would
not
. Tests located in in resolved dependencies are
not executed.
You can be specific in which tests are executed in a run. To filter a test run,
filter by specifying one or more fully qualified class name as arguments
to the zero test CLI task as follows:
zero test my.custom.TestingClass my.util.MyTest
Tests are not filtered by class name when specific tests are provided as arguments this way.
Alternate patterns for including classes may be specified in zero.config as a
list of regex patterns under /config/test/classes.
For example:
/config/test/classes = ["(.*)Test"]
Java classes must be compiled before invoking the zero test task.
A common workflow might be:
... edit java code ... zero compile zero test ... view results ... ... edit java code and repeat ...
JUnit tests written in Groovy work just like traditional Java JUnit tests. Class
name filtering using the /config/test/classes or CLI arguments.
However, the bindings available in application code are not.
Groovy tests must be located in the /app/scripts folder of the
testing module.
Writing tests
The zero.test module provides some utilities as a convenience
when writing tests. Note that while examples show Java samples, use in Groovy
also applies.
The application launcher
The application launcher starts a WebSphere sMash application in the current executing
Java process allowing you to test your application programmatically. Use the application launcher
by importing the zero.util.launcher.ApplicationLauncher class into
your JUnit test, creating a named instance (useful for logging), and calling the
startApplication() method.
Once an application is started, the JUnit test can send HTTP requests and make assertions on both the server and client state. Such tests can include but are not limited to:
- HTTP response (e.g. test response headers)
- In-container while processing an HTTP request (e.g. test values in the /request zone)
- Dependent on a configured Global Context, but not a running application (e.g. JSON APIs (custom converters are registered via GC))
The following sections provide code samples that illustrate each of these patterns.
Tests of HTTP response
// File: app/scripts/example/ResponseTest.groovy
package example
import static org.junit.Assert.assertEquals
import java.net.HttpURLConnection
import java.net.URL
import org.junit.Test
import zero.core.context.GlobalContext
import zero.core.context.GlobalContextURIs.Config
import zero.util.http.HttpConnectionContentHandlerFactory
import zero.util.launcher.ApplicationLauncher
class ResponseTest implements zero.core.groovysupport.ZeroObject {
@Test
void testResponseEntity() {
// Utility handler factory supports text/plain and text/html (returns content as String),
// application/json and text/json (returns result of Json.decode),
// and application/xml (returns instance of org.w3c.dom.Document).
HttpConnectionContentHandlerFactory.register()
// ApplicationLauncher is a utility for embedding WebSphere sMash applications
// within test cases.
ApplicationLauncher launcher = new ApplicationLauncher("testResponseEntity", ".")
try {
// startApplication() returns when the application is ready for requests.
launcher.startApplication()
// At this point, the Global Context is fully configured. It's good practice
// to leverage the GC for configurable settings, rather than hard coding values
// like the HTTP port.
URL u = new URL("http://localhost:" + config.http.port[] + "/response.groovy")
HttpURLConnection conn = (HttpURLConnection) u.openConnection()
assertEquals("entity", conn.content)
} finally {
launcher.stopApplication()
}
}
}
// File: public/response.groovy print "entity"
GlobalContext.zget("/config/http/port") can be shortened to
config.http.port[]. To get access to the bindings, set the marker interface on
the JUnit test case class of zero.core.groovysupport.ZeroObject as illustrated
above.
Tests running in-container
In-container tests also use zero.util.launcher.ApplicationLauncher to embed the application within the test case. To support in-container assertions, requests are sent via the zero.test.TestUtils.sendTestRequest method. This utility method handles the setup required to project assertion errors from the container thread back to the test client. For example:
// File: app/scripts/example/InContainerTest.groovy
package example
import java.net.HttpURLConnection
import java.net.URL
import org.junit.Test
import zero.core.context.GlobalContext
import zero.core.context.GlobalContextURIs.Config
import zero.util.launcher.ApplicationLauncher
class InContainerTest {
@Test
void testInContainer() {
// ApplicationLauncher is a utility for embedding WebSphere sMash applications
// within test cases.
ApplicationLauncher launcher = new ApplicationLauncher("testInContainer", ".")
try {
// startApplication() returns when the application is ready for requests.
launcher.startApplication()
// At this point, the Global Context is fully configured. It's good practice
// to leverage the GC for configurable settings, rather than hard coding values
// like the HTTP port.
URL u = new URL("http://localhost:" + config.http.port[] + "/incontainer.groovy")
HttpURLConnection conn = (HttpURLConnection) u.openConnection()
conn.setRequestProperty("foo", "bar")
// Process the request via a utility method that handles the setup required to
// project assertion errors from the container thread back to the test client.
TestUtils.sendTestRequest(conn)
} finally {
launcher.stopApplication()
}
}
}
// File: public/incontainer.groovy assert 'GET' == request.method[] assert 'bar' == request.headers.in.foo[]
If the /config/exitCode of the application is set but not equal to zero at the end of startApplication, the startApplication method will throw a RuntimeException. For this
reason you should always call stopApplication within a finally block (as shown above) to ensure the application is fully stopped.
Tests dependent on a configured Global Context, but not a running application
The zero.core.config.ConfigLoader utility class provides APIs for creating a new Global Context and loading config files for a specified module and all its dependencies. For example:
// File: app/scripts/example/GCDependentTest.groovy
package example
import static org.junit.Assert.assertEquals
import org.junit.Test
import zero.core.config.ConfigLoader
import zero.json.Json
class GCDependentTest {
@Test
void testJavaUtilDate() {
// Json.encode and .decode depend on the configured GC for custom converters,
// including converters for java.util.Date.
// ConfigLoader.initialize creates a new Global Context and loads config files
// for the specified module and all its dependencies.
ConfigLoader.initialize(".")
java.util.Date d = new java.util.Date()
String jsonStr = Json.encode(d)
assertEquals("Failed to match Date", d, Json.toObject(Json.decode(jsonStr), java.util.Date.class))
}
}
Testing tasks
WebSphere sMash provides an open framework to implement CLI tasks. In fact,
the zero test is implemented using this framework. The
zero.test module provides a utility to check for success and
failure of your custom task.
Some of the conveniences it provides is setting up logging for the invoked CLI task, asserting that the task succeeded or failed with specific messages displayed on the console.
The abstract zero.test.TaskTester class must be implemented to use
this utility. Simply extend it and provide your custom task's usage message
when a user does not use the task as documented as a list of strings. Each
string represents a line in the message.
Following are examples on some of its usage, for a
complete reference, view the zero.test.TaskTester Javadoc.
// File: app/scripts/example/CustomTaskTest.groovy
package example
import java.util.List
import java.util.ArrayList
import zero.test.TaskTester
import org.junit.BeforeClass
import org.junit.Test
public class CustomTaskTest extends TaskTester {
List<String> getUsageStrings() {
return ["USAGE:", "zero custom correct-usage [--optionalflag]"]
}
@BeforeClass
static void ensureLogging() {
setupLogging()
}
@Test
void testFailureAndUsage() {
failsWithUsage("custom incorrect-usage")
}
@Test
void testCustomMessage() {
runForFailure("custom dumpdata --optionalflag=somevalue",
"--optionalflag cannot have a value");
}
@Test
public void testSuccess() {
runForSuccess("custom correct-usage",
"Custom success message")
runForSuccess("custom correct-usage --optionalflag",
"Custom success message with optional flag")
}
}