-
Notifications
You must be signed in to change notification settings - Fork 0
Testing Tutorial
This tutorial demonstrates how to write a test using xenon testing framework.
We use ExampleService
, ExampleTaskService
, and ExampleServiceHost
to demonstrate the topic. These classes are also available in xenon testing framework module.
All xenon testing framework classes are packaged in xenon-common-[version]-tests.jar
.
for maven:
<dependency>
<groupId>com.vmware.xenon</groupId>
<artifactId>xenon-common</artifactId>
<version>${xenon.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
To test ExampleService
and ExampleTaskService
, we use ExampleServiceHost
which is a ServiceHost
implementation that simply starts those two services.
// use junit TemporaryFolder rule to create random working directory for ServiceHost
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
private ExampleServiceHost createAndStartExampleServiceHost() throws Throwable {
Arguments args = new Arguments();
args.sandbox = tempFolder.newFolder().toPath(); // use random directory
args.port = 0; // use random port
ExampleServiceHost host = new ExampleServiceHost();
host.initialize(args);
host.start();
return host;
}
ExampleServiceHost host1 = createAndStartExampleServiceHost();
ExampleServiceHost host2 = createAndStartExampleServiceHost();
Since host1
and host2
are independently started, they have to know each other by joining same node group.
Also, we have to wait the factory service to be ready in the node group because ExampleService
and ExampleTaskService
are PERSISTENCE
, OWNER_SELECTION
services.
TestNodeGroupManager nodeGroup = new TestNodeGroupManager()
.addHost(host1)
.addHost(host2);
nodeGroup.joinNodeGroupAndWaitForConvergence(); // join to default node group
nodeGroup.waitForFactoryServiceAvailable("/core/examples");
Once node group is converged and factory service is ready for the replicated service, it is time to perform tests by creating service instances and validating them.
In test, it certainly makes code readable if code is written in sequential manner; therefore, xenon test framework provides TestRequestSender
that has methods to send Operation(s)
and receive response(s) synchronously.
First, let's make a POST request to the factory to create a service.
We use static documentSelfLink
to make it easy to validate the target service.
// create request sender using one of the hosts in the nodegroup
TestRequestSender sender = new TestRequestSender(nodeGroup.getHost());
ExampleServiceState postBody = new ExampleServiceState();
postBody.name = "foo";
postBody.counter = 0L;
postBody.documentSelfLink = "foo"; // static self link: /core/examples/foo
Operation post = Operation.createPost(host1, "/core/examples").setBody(postBody);
// send POST and wait for response, then validate
ExampleServiceState postResult = sender.sendAndWait(post, ExampleServiceState.class);
assertEquals("foo", postResult.name);
assertEquals("/core/examples/foo", postResult.documentSelfLink);
TestRequestSender
also provide methods to send multiple Operations
asynchronously then wait until all of them returns response.
Let's send multiple PATCH requests and check the counter value.
The implementation of handlePut
in ExampleService
makes sure the max of the value is set to the counter.
List<Operation> patches = new ArrayList<>();
for (int i = 0; i < 20; i++) {
ExampleServiceState patchBody = new ExampleServiceState();
patchBody.name = "foo-" + i;
patchBody.counter = (long) (i + 1); // 1-20
Operation patch = Operation.createPatch(host1, "/core/examples/foo").setBody(patchBody);
patches.add(patch);
}
// send 20 PATCH requests in parallel and wait all responses to be successful
sender.sendAndWait(patches);
// verify latest state
Operation get = Operation.createGet(host1, "/core/examples/foo");
ExampleServiceState getResult = sender.sendAndWait(get, ExampleServiceState.class);
assertEquals(Long.valueOf(20), getResult.counter);
Sometimes, you want to expect the request to fail.
TestRequestSender#sendAndWaitFailure
expects failure response and returns FailureResponse
object.
// delete foo service
Operation delete = Operation.createDelete(host1, "/core/examples/foo").setBody("{}");
sender.sendAndWait(delete);
// verify call to foo returns 404
Operation getToDeleted = Operation.createGet(host1, "/core/examples/foo");
FailureResponse failureResponse = sender.sendAndWaitFailure(getToDeleted);
assertEquals(404, failureResponse.op.getStatusCode());
In xenon, everything happens in async. Thus, you cannot simply expect work has done right after the operation has completed.
While writing tests, it requires certain logic to check whether your target action has finished or not. TestContext#waitFor
or VerificationHost#waitFor
provide a mechanism to wait until supplied check logic satisfies the condition.
To demonstrate the wait condition, let's write another test case using ExampleTaskService
.
First, let's prepare host
and sender
, then make a POST request to create a ExampleTaskService
.
ExampleServiceHost host = createAndStartExampleServiceHost();
TestRequestSender sender = new TestRequestSender(host);
// create a task
ExampleTaskServiceState postBody = new ExampleTaskServiceState();
postBody.documentSelfLink = "bar";
Operation post = Operation.createPost(host, "/core/example-tasks").setBody(postBody);
sender.sendAndWait(post, ExampleTaskServiceState.class);
Since it is a task service, it'll go through its workflow(state machine); so, we wait the task to finish.
We use TestContext#waitFor
method.
// repeat given lambda until it returns true. timeout is set to 10sec.
waitFor(Duration.ofSeconds(10), () -> {
Operation get = Operation.createGet(host, "/core/example-tasks/bar");
ExampleTaskServiceState result = sender.sendAndWait(get, ExampleTaskServiceState.class);
return isInFinalStage(result);
}, () -> "bar didn't reach the final stage");
...
private boolean isInFinalStage(ExampleTaskServiceState state) {
EnumSet<TaskStage> finalStages = EnumSet.of(CANCELLED, FAILED, FINISHED);
return state.taskInfo != null && finalStages.contains(state.taskInfo.stage);
}
Once wait logic has completed, the task has finished.
Let's verify the finished task.
Operation get = Operation.createGet(host, "/core/example-tasks/bar");
ExampleTaskServiceState result = sender.sendAndWait(get, ExampleTaskServiceState.class);
assertEquals(FINISHED, result.taskInfo.stage);
// verify more...
Test Code
public class SampleTest {
// use junit TemporaryFolder rule to create random working directory for ServiceHost
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void verifyExampleService() throws Throwable {
ExampleServiceHost host1 = createAndStartExampleServiceHost();
ExampleServiceHost host2 = createAndStartExampleServiceHost();
TestNodeGroupManager nodeGroup = new TestNodeGroupManager()
.addHost(host1)
.addHost(host2);
nodeGroup.joinNodeGroupAndWaitForConvergence(); // join to default node group
nodeGroup.waitForFactoryServiceAvailable("/core/examples");
// create request sender using one of the hosts in the nodegroup
TestRequestSender sender = new TestRequestSender(nodeGroup.getHost());
ExampleServiceState postBody = new ExampleServiceState();
postBody.name = "foo";
postBody.counter = 0L;
postBody.documentSelfLink = "foo"; // static self link: /core/examples/foo
Operation post = Operation.createPost(host1, "/core/examples").setBody(postBody);
// send POST and wait for response, then validate
ExampleServiceState postResult = sender.sendAndWait(post, ExampleServiceState.class);
assertEquals("foo", postResult.name);
assertEquals("/core/examples/foo", postResult.documentSelfLink);
List<Operation> patches = new ArrayList<>();
for (int i = 0; i < 20; i++) {
ExampleServiceState patchBody = new ExampleServiceState();
patchBody.name = "foo-" + i;
patchBody.counter = (long) (i + 1); // 1-20
Operation patch = Operation.createPatch(host1, "/core/examples/foo").setBody(patchBody);
patches.add(patch);
}
// send 20 PATCH requests in parallel and wait all responses to be successful
sender.sendAndWait(patches);
Operation get = Operation.createGet(host1, "/core/examples/foo");
ExampleServiceState getResult = sender.sendAndWait(get, ExampleServiceState.class);
assertEquals(Long.valueOf(20), getResult.counter);
// delete foo service
Operation delete = Operation.createDelete(host1, "/core/examples/foo").setBody("{}");
sender.sendAndWait(delete);
// verify call to foo returns 404
Operation getToDeleted = Operation.createGet(host1, "/core/examples/foo");
FailureResponse failureResponse = sender.sendAndWaitFailure(getToDeleted);
assertEquals(404, failureResponse.op.getStatusCode());
}
private ExampleServiceHost createAndStartExampleServiceHost() throws Throwable {
Arguments args = new Arguments();
args.sandbox = tempFolder.newFolder().toPath(); // use random directory
args.port = 0; // use random port
ExampleServiceHost host = new ExampleServiceHost();
host.initialize(args);
host.start();
return host;
}
@Test
public void verifyExampleTaskService() throws Throwable {
ExampleServiceHost host = createAndStartExampleServiceHost();
TestRequestSender sender = new TestRequestSender(host);
// create a task
ExampleTaskServiceState postBody = new ExampleTaskServiceState();
postBody.documentSelfLink = "bar";
Operation post = Operation.createPost(host, "/core/example-tasks").setBody(postBody);
sender.sendAndWait(post, ExampleTaskServiceState.class);
// repeat given lambda until it returns true. timeout is set to 10sec.
waitFor(Duration.ofSeconds(10), () -> {
Operation get = Operation.createGet(host, "/core/example-tasks/bar");
ExampleTaskServiceState result = sender.sendAndWait(get, ExampleTaskServiceState.class);
return isInFinalStage(result);
}, () -> "bar didn't reach the final stage");
Operation get = Operation.createGet(host, "/core/example-tasks/bar");
ExampleTaskServiceState result = sender.sendAndWait(get, ExampleTaskServiceState.class);
assertEquals(FINISHED, result.taskInfo.stage);
// verify more...
}
private boolean isInFinalStage(ExampleTaskServiceState state) {
EnumSet<TaskStage> finalStages = EnumSet.of(CANCELLED, FAILED, FINISHED);
return state.taskInfo != null && finalStages.contains(state.taskInfo.stage);
}
}