Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Art of Java-Based HTTP Data Service Implementation and Testing (Part 1)

DZone's Guide to

The Art of Java-Based HTTP Data Service Implementation and Testing (Part 1)

Let's get started with end-to-end service testing and see how ActFramework can make it easy to test your Java-based HTTP data services.

· Java Zone ·
Free Resource

FlexNet Code Aware, a free scan tool for developers. Scan Java, NuGet, and NPM packages for open source security and open source license compliance issues.

I encourage you to follow the steps listed in this article and practice on your own computer. I guarantee it will be a nice and easy trip for you.

For the last few years, more and more systems have applied a separated frontend/backend approach that lets frontend tech like Angular/React/Vue handle the presentation layer while leaving the backend a pure data service.

In the world of Java, a lot of frameworks provide support for implementing HTTP data services, including Spring Boot, Jersey, and Play Framework; with fewer libraries for HTTP data service testing like Karate, REST assured, etc. In this article, we will introduce ActFramework and show how it is different in terms of expressiveness and simplicity compared with other frameworks/libraries for both HTTP data service implementation and testing.

So let's get started with the following Maven command:

luog@luog-X510UQR:/tmp/1$ mvn archetype:generate -B \
>     -DgroupId=com.mycom.helloservice \
>     -DartifactId=helloservice \
>     -DarchetypeGroupId=org.actframework \
>     -DarchetypeArtifactId=archetype-simple-restful-service \
>     -DarchetypeVersion=1.8.8.1


Run the above command you should be able to see a Java project has been generated:

[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> maven-archetype-plugin:3.0.1:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO] 
[INFO] <<< maven-archetype-plugin:3.0.1:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO] 
[INFO] 
[INFO] --- maven-archetype-plugin:3.0.1:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Batch mode
[INFO] Archetype repository not defined. Using the one from [org.actframework:archetype-simple-restful-service:1.8.7.3] found in catalog remote
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: archetype-simple-restful-service:1.8.8.1
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.mycom.helloservice
[INFO] Parameter: artifactId, Value: helloservice
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.mycom.helloservice
[INFO] Parameter: packageInPathFormat, Value: com/mycom/helloservice
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.mycom.helloservice
[INFO] Parameter: groupId, Value: com.mycom.helloservice
[INFO] Parameter: artifactId, Value: helloservice
[INFO] Executing META-INF/archetype-post-generate.groovy post-generation script
[INFO] Project created from Archetype in dir: /tmp/1/helloservice
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.439 s
[INFO] Finished at: 2018-05-14T20:48:58+10:00
[INFO] Final Memory: 21M/215M
[INFO] ------------------------------------------------------------------------


Take a quick inspection of the file structure that has been generated:

luog@luog-X510UQR:/tmp/1$ tree helloservice/
helloservice/
├── pom.xml
├── run_dev
├── run_dev.bat
├── run_e2e
├── run_e2e.bat
├── run_prod
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── mycom
    │   │           └── helloservice
    │   │               ├── AppEntry.java
    │   │               └── Service.java
    │   └── resources
    │       ├── com
    │       │   └── mycom
    │       │       └── helloservice
    │       ├── conf
    │       │   ├── app.properties
    │       │   ├── prod
    │       │   │   └── app.properties
    │       │   └── uat
    │       │       └── app.properties
    │       ├── e2e
    │       │   └── scenarios.yml
    │       ├── logback.xml
    │       └── rythm
    │           └── com
    │               └── mycom
    │                   └── helloservice
    └── test
        └── java
            └── com
                └── mycom
                    └── helloservice

23 directories, 13 files


It's easy to see that the project uses the standard Maven Java project structure, with a few scripts generated in the project root dir. We will skip those script files for now and take a look at the Java classes generated. First, let's check the AppEntry.java file:

package com.mycom.helloservice;

import act.Act;

/**
 * A simple hello world service app entry
 *
 * Run this app, try to update some of the code, then
 * press F5 in the browser to watch the immediate change
 * in the browser!
 */
@SuppressWarnings("unused")
public class AppEntry {

    public static void main(String[] args) throws Exception {
        Act.start();
    }

}


A very simple Java class, isn't it? With a single line in the main method, this class is responsible for bootstrapping ActFramework, which will scan all Java classes under the com.mycom.helloservice package.

The second Java file Service.java defines service endpoints provided by this application:

package com.mycom.helloservice;

import act.inject.DefaultValue;
import org.joda.time.DateTime;
import org.osgl.mvc.annotation.GetAction;

public class Service {

    /**
     * The hello (`/hello`) endpoint.
     *
     * This will accept a query parameter named `who` and
     * return a greeting string in a form of "Hello $who"
     *
     * @param who
     *      request query parameter to specify the hello target.
     *      default value is `World`.
     * @return A hello string
     */
    @GetAction("hello")
    public String hello(@DefaultValue("World") String who) {
        return "Hello " + who;
    }

    /**
     * Returns an important date in history: 09/Mar/2016.
     *
     * [AlphaGo](https://en.wikipedia.org/wiki/AlphaGo), a computer program defeated
     * [Lee Sedol](https://en.wikipedia.org/wiki/Lee_Sedol), one of the best players at Go
     * at this date.
     *
     * @return an important date in the history
     */
    @GetAction("date")
    public DateTime date() {
        return DateTime.parse("2016-03-09");
    }

}


These two endpoints are simple and easy to understand.

Now let's have some fun and run the application. You can do that by either running the ./run_dev script or mvn compile act:run — they are the exact same thing. Once you have typed any one of the commands, you should be able to see that the application has been started and is listening to port 5460, the default port of ActFramework:

Listening for transport dt_socket at address: 5005
       _           _    __   _   _         ___   _   _ 
 |_|  |_  |   |   / \  (_   |_  |_)  \  /   |   /   |_ 
 | |  |_  |_  |_  \_/  __)  |_  | \   \/   _|_  \_  |_ 

               powered by ActFramework r1.8.8-RC4-aa2d4

 version: v1.0-SNAPSHOT-180514_2102
scan pkg: 
base dir: /tmp/1/helloservice
     pid: 15988
 profile: dev
    mode: DEV

     zen: Readability counts.

2018-05-14 21:02:58,308 INFO  a.Act@[main] - loading application(s) ...
2018-05-14 21:02:58,314 INFO  a.a.App@[main] - App starting ....
2018-05-14 21:02:58,485 WARN  a.h.b.ResourceGetter@[main] - URL base not exists: META-INF/resources/webjars
2018-05-14 21:02:58,497 WARN  a.a.DbServiceManager@[main] - DB service not initialized: No DB plugin found
2018-05-14 21:02:59,139 WARN  a.m.MailerConfig@[main] - smtp host configuration not found, will use mock smtp to send email
2018-05-14 21:02:59,449 INFO  a.a.App@[main] - App[helloservice] loaded in 1135ms
2018-05-14 21:02:59,453 INFO  a.a.ApiManager@[jobs-thread-3] - start compiling API book
2018-05-14 21:02:59,475 INFO  o.xnio@[main] - XNIO version 3.3.8.Final
2018-05-14 21:02:59,501 INFO  o.x.nio@[main] - XNIO NIO Implementation Version 3.3.8.Final
2018-05-14 21:02:59,625 INFO  a.Act@[main] - network client hooked on port: 5460
2018-05-14 21:02:59,627 INFO  a.Act@[main] - CLI server started on port: 5461
2018-05-14 21:02:59,628 INFO  a.Act@[main] - app is ready at: http://192.168.1.2:5460
2018-05-14 21:02:59,628 INFO  a.Act@[main] - it takes 3014ms to start the app


Open your browser and visit the two endpoints defined in the Service.java file:

First GET /hello:
GET /hello

Second GET /hello with the query parameter who specified:
GET /hello?who=DZone

Third GET /date:
GET /date


We have used the browser to verify that our data service endpoints work as expected. However, using the browser to verify the service endpoints isn't a good idea because:

  1. It takes a lot of our time to run regression test on endpoints
  2. It is unreliable as people make mistakes or ignore things

Fortunately, ActFramework provides the capability to support defining and running end-to-end tests. Before we start inspecting the test case definition, let's first try running it and see how it works. To run end-to-end test for an ActFramework application, developers can either type ./run_e2e or mvn compile act:e2e:

luog@luog-X510UQR:/tmp/1/helloservice$ mvn compile act:e2e
Picked up JAVA_TOOL_OPTIONS: -Djava.security.egd=file:/dev/./urandom -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true -Dswing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel -Djava2d.font.loadFontConf=true
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building My Awesome Application 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-enforcer-plugin:1.0:enforce (enforce-maven) @ helloservice ---
[INFO] 
[INFO] --- buildnumber-maven-plugin:1.4:create-timestamp (default) @ helloservice ---
[INFO] 
[INFO] --- buildnumber-maven-plugin:1.4:create (default) @ helloservice ---
[INFO] ShortRevision tag detected. The value is '4'.
[INFO] Executing: /bin/sh -c cd '/tmp/1/helloservice' && 'git' 'rev-parse' '--verify' '--short=4' 'HEAD'
[INFO] Working directory: /tmp/1/helloservice
[INFO] Storing buildNumber: 180514_2241 at timestamp: yyMMdd_HHmm
[WARNING] Cannot get the branch information from the git repository: 
Detecting the current branch failed: fatal: Not a git repository (or any of the parent directories): .git

[INFO] ShortRevision tag detected. The value is '4'.
[INFO] Executing: /bin/sh -c cd '/tmp/1/helloservice' && 'git' 'rev-parse' '--verify' '--short=4' 'HEAD'
[INFO] Working directory: /tmp/1/helloservice
[INFO] Storing buildScmBranch: UNKNOWN_BRANCH
[INFO] 
[INFO] --- jacoco-maven-plugin:0.7.9:prepare-agent (default) @ helloservice ---
[INFO] argLine set to -javaagent:/home/luog/.m2/repository/org/jacoco/org.jacoco.agent/0.7.9/org.jacoco.agent-0.7.9-runtime.jar=destfile=/tmp/1/helloservice/target/jacoco.exec
[INFO] 
[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ helloservice ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 6 resources
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-dependency-plugin:3.0.2:unpack (unpack) @ helloservice ---
[INFO] Configured Artifact: org.actframework:act-starter-support:1.8.6:jar
[INFO] act-starter-support-1.8.6.jar already unpacked.
[INFO] 
[INFO] --- maven-compiler-plugin:3.7.0:compile (default-compile) @ helloservice ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:3.0.2:copy-resources (copy-scripts) @ helloservice ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 4 resources
[INFO] 
[INFO] --- maven-resources-plugin:3.0.2:copy-resources (copy-dockerfile) @ helloservice ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] 
[INFO] --- act-maven-plugin:1.8.8.0:e2e (default-cli) @ helloservice ---
[INFO] Listening for jpda connection at 5005
Picked up JAVA_TOOL_OPTIONS: -Djava.security.egd=file:/dev/./urandom -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true -Dswing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel -Djava2d.font.loadFontConf=true
Listening for transport dt_socket at address: 5005
       _           _    __   _   _         ___   _   _ 
 |_|  |_  |   |   / \  (_   |_  |_)  \  /   |   /   |_ 
 | |  |_  |_  |_  \_/  __)  |_  | \   \/   _|_  \_  |_ 

               powered by ActFramework r1.8.8-RC4-aa2d4

 version: v1.0-SNAPSHOT-180514_2241
scan pkg: 
base dir: /tmp/1/helloservice
     pid: 27144
 profile: e2e
    mode: DEV

     zen: Simple things should be simple, complex things should be possible.

2018-05-14 22:41:41,142 INFO  a.Act@[main] - loading application(s) ...
2018-05-14 22:41:41,153 INFO  a.a.App@[main] - App starting ....
2018-05-14 22:41:41,380 WARN  a.h.b.ResourceGetter@[main] - URL base not exists: META-INF/resources/webjars
2018-05-14 22:41:41,394 WARN  a.a.DbServiceManager@[main] - DB service not initialized: No DB plugin found
2018-05-14 22:41:42,044 WARN  a.m.MailerConfig@[main] - smtp host configuration not found, will use mock smtp to send email
2018-05-14 22:41:42,408 INFO  a.a.App@[main] - App[helloservice] loaded in 1255ms
2018-05-14 22:41:42,412 INFO  a.a.ApiManager@[jobs-thread-3] - start compiling API book
2018-05-14 22:41:42,519 INFO  o.xnio@[main] - XNIO version 3.3.8.Final
2018-05-14 22:41:42,538 INFO  o.x.nio@[main] - XNIO NIO Implementation Version 3.3.8.Final
2018-05-14 22:41:42,663 INFO  a.Act@[main] - network client hooked on port: 5460
2018-05-14 22:41:42,664 INFO  a.Act@[main] - CLI server started on port: 5461
2018-05-14 22:41:42,666 INFO  a.Act@[main] - app is ready at: http://192.168.1.2:5460
2018-05-14 22:41:42,666 INFO  a.Act@[main] - it takes 2264ms to start the app

Start running E2E test scenarios

================================================================================
HELLO SERVICE

a service says hello
--------------------------------------------------------------------------------
[PASS] send request to hello service without parameter
[PASS] send request to hello servcie with parameter specified
[PASS] send request to hello servcie with parameter specified and require JSON response
--------------------------------------------------------------------------------
It takes 0s to run this scenario.

================================================================================
DATE SERVICE

A service returns an important date in the history
--------------------------------------------------------------------------------
[PASS] send request to the service
[PASS] send request to the service and request response be JSON format
--------------------------------------------------------------------------------
It takes 0s to run this scenario.

2018-05-14 22:41:43,774 INFO  a.a.App@[jobs-thread-4] - App shutting down ....
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.548 s
[INFO] Finished at: 2018-05-14T22:41:44+10:00
[INFO] Final Memory: 24M/313M
[INFO] ------------------------------------------------------------------------


As shown above, running e2e tests will:

  1. Start the application
  2. Run e2e test scenarios
  3. Shut down the application

Now let's open the src/main/resources/e2e/scenarios.yml file, which defines the two end-to-end test scenarios for this application:

Scenario(Hello Service):
  description: a service says hello
  interactions:
    - description: send request to hello service without parameter
      request:
        method: GET
        url: /hello
      response:
        text: Hello World # response text must be "Hello World"
    - description: send request to hello servcie with parameter specified
      request:
        method: GET
        url: /hello?who=ActFramework
      response:
        # this time we demonstrate how to verify text with a list of verifiers
        text:
          - eq: Hello ActFramework # value must be equal to "Hello ActFramework"
          - contains: ActFramework # value must contains "ActFramework"
          - starts: Hello # value must starts with "Hello"
          - ends: Framework # value must ends with "Framework"
    - description: send request to hello servcie with parameter specified and require JSON response
      request:
        json: true # specify accept type is application/json
        method: GET
        url: /hello?who=Java
      response:
        json: # treat result as a JSON object
          # act returns json result in `{"result": ...}` style
          result: Hello Java # result property of the JSON object must be "Hello World"

# Test Service#date() endpoint, which is available at `GET /date` endpoint
Scenario(Date Service):
  description: A service returns an important date in the history
  interactions:
    - description: send request to the service
      request:
        method: GET
        url: /date
      response:
        text:
          - after: 1997-05-11 # the returned date should be after date 1997-05-11
          - before: 13/May/2018 # the returned date should be before date 13/May/2018
    - description: send request to the service and request response be JSON format
      request:
        json: true
        method: GET
        url: /date
      response:
        json: # treat result as a JSON object
          # act returns json result in `{"result": ...}` style
          result:  # thus we will use `result` to fetch the date
            - after: 1997-05-11 # the returned date should be after date 1997-05-11
            - before: 13/May/2018 # the returned date should be before date 13/May/2018


The scenario file is pretty straightforward — it literally organizes multiple end-to-end test scenarios in the following way:

  • The first level object defines testing Scenarios. We put the name of the Scenario in the parentheses next to the word Scenario, like Scenario(Hello Service)
  • Within each Scenario, it defines a set of Interactions, which itself contains
    • A description
    • A request specification
      • Defines the request we need to send to the data endpoint we want to test
    • A response specification
      • Defines how to verify the response

Summary

In this article, we have demonstrated:

  1. How to use Maven archetypes to create a new ActFramework application scaffold.

  2. The generated project structure and source code.

  3. How to run the application

  4. How to test the application.

  5. The end-to-end test scenario definition.

I hope by following the steps described in this article, you can get some understanding of the simplicity and expressiveness of ActFramework, and I hope you enjoy your journey.

This is the first part of this ActFramework data service series. In the next article, we will revisit this project, add a more complicated endpoint, and write the new test scenario for it.

 Scan Java, NuGet, and NPM packages for open source security and license compliance issues. 

Topics:
java ,test automation ,end to end testing ,actframework ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}