Generic Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this article, we will introduce the ZeroCode automated testing framework. We'll learn the fundamentals through an example of REST API testing.

2. The Approach

The ZeroCode framework takes the following approaches:

  • Multi-faceted testing support
  • The declarative style of testing

Let's discuss them both.

2.1. Multi-Faceted Testing Support

The framework is designed to support automated testing of multiple facets of our applications. Among other things, it gives us the ability to test:

  • REST
  • SOAP
  • Security
  • Load/Stress
  • Database
  • Apache Kafka
  • GraphQL
  • Open API Specifications

Testing is done via the framework's DSL that we will discuss shortly.

2.2. Declarative Style

ZeroCode uses a declarative style of testing, which means that we don't have to write actual testing code. We declare scenarios in JSON/YAML files, and the framework will ‘translate' them into test code behind-the-scenes. This helps us to concentrate on what we want to test instead of how to test it.

3. Setup

Let's add the Maven dependency in our pom.xml file:

 <dependency>
      <groupId>org.jsmart</groupId>
      <artifactId>zerocode-tdd</artifactId>
      <version>1.3.27</version>
      <scope>test</scope>
 </dependency>

The latest version is available on Maven Central. We can use Gradle as well. If we're using IntelliJ, we can download the ZeroCode plugin from Jetbrains Marketplace.

4. REST API Testing

As we said above, ZeroCode can support testing multiple parts of our applications. In this article, we'll focus on REST API testing. For that reason, we'll create a small Spring Boot web app and expose a single endpoint:

@PostMapping
public ResponseEntity create(@RequestBody User user) {
    if (!StringUtils.hasText(user.getFirstName())) {
        return new ResponseEntity("firstName can't be empty!", HttpStatus.BAD_REQUEST);
    }
    if (!StringUtils.hasText(user.getLastName())) {
        return new ResponseEntity("lastName can't be empty!", HttpStatus.BAD_REQUEST);
    }
    user.setId(UUID.randomUUID().toString());
    users.add(user);
    return new ResponseEntity(user, HttpStatus.CREATED);
}

Let's see the User class that's referenced in our controller:

public class User {
    private String id;
    private String firstName;
    private String lastName;

    // standard getters and setters
}

When we create a user, we set a unique id and return the whole User object back to the client. The endpoint is reachable at the /api/users path. We'll be saving users in memory to keep things simple.

5. Writing a Scenario

The scenario plays a central role in ZeroCode. It consists of one or more steps, which are the actual things we want to test. Let's write a scenario with a single step that tests the successful path of user creation:

{
  "scenarioName": "test user creation endpoint",
  "steps": [
    {
      "name": "test_successful_creation",
      "url": "/api/users",
      "method": "POST",
      "request": {
        "body": {
          "firstName": "John",
          "lastName": "Doe"
        }
      },
      "verify": {
        "status": 201,
        "body": {
          "id": "$NOT.NULL",
          "firstName": "John",
          "lastName": "Doe"
        }
      }
    }
  ]
}

Let's explain what each of the properties represents:

  • scenarioName – This is the name of the scenario; we can use any name we want
  • steps – an array of JSON objects, where we describe what we want to test
    • name –  the name that we give to the step
    • url –  relative URL of the endpoint; we can also put an absolute URL, but generally, it's not a good idea
    • method – HTTP method
    • request – HTTP request
      • body – HTTP request body
    • verify – here, we verify/assert the response that the server returned
      • status – HTTP response status code
      • body (inside verify property) – HTTP response body

In this step, we check if user creation is successful. We check the firstName and lastName values that the server returns. Also, we verify that the id is not null with ZeroCode's assertion token.

Generally, we have more than one step in scenarios. Let's add another step inside our scenario's steps array:

{
  "name": "test_firstname_validation",
  "url": "/api/users",
  "method": "POST",
  "request": {
    "body": {
      "firstName": "",
      "lastName": "Doe"
    }
  },
  "verify": {
    "status": 400,
    "rawBody": "firstName can't be empty!"
  }
}

In this step, we supply an empty first name that should result in a bad request. Here, the response body will not be in JSON format, so we use the rawbody property to refer to it as a plain string.

ZeroCode can't directly run the scenario — for that, we need a corresponding test case.

6. Writing a Test Case

To execute our scenario, let's write a corresponding test case:

@RunWith(ZeroCodeUnitRunner.class)
@TargetEnv("rest_api.properties")
public class UserEndpointIT {

    @Test
    @Scenario("rest/user_create_test.json")
    public void test_user_creation_endpoint() {
    }
}

Here, we declare a method and mark it as a test using the @Test annotation from JUnit 4. We can use JUnit 5 with ZeroCode for load testing only.

We also specify the location of our scenario with the @Scenario annotation, which comes from the ZeroCode framework. The method body is empty. As we said, we don't write actual testing code. What we want to test is described in our scenario. We just reference the scenario in a test case method. Our UserEndpointIT class has two annotations:

  • @RunWith – here, we specify which ZeroCode class is responsible for running our scenarios
  • @TargetEnv – this points to the property file that will be used when our scenario runs

When we declared the url property above, we specified the relative path. Obviously, the ZeroCode framework needs an absolute path, so we create a rest_api.properties file with a few properties that define how our test should run:

  • web.application.endpoint.host – the host of REST API; In our case, it's http://localhost
  • web.application.endpoint.port – the port of the application server where our REST API is exposed; In our case, it's 8080
  • web.application.endpoint.context – the context of the API; in our case, it's empty

The properties that we declare in the property file depend on what kind of testing we're doing. For example, if we want to test a Kafka producer/consumer, we'll have different properties.

7. Executing a Test

We've created a scenario, property file, and test case. Now, we're ready to run our test. Since ZeroCode is an integration testing tool, we can leverage Maven's failsafe plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.0.0-M5</version>
    <dependencies>
        <dependency>
            <groupId>org.apache.maven.surefire</groupId>
            <artifactId>surefire-junit47</artifactId>
            <version>3.0.0-M5</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

To run the test, we can use the following command:

mvn verify -Dskip.it=false

 ZeroCode creates multiple types of logs that we can check out in the ${project.basedir}/target folder.

8. Conclusion

In this article, we took a look at the ZeroCode automated testing framework. We showed how the framework works with the example of REST API testing. We also learned that ZeroCode DSL eliminates the need for writing actual testing code.

As always, the source code of the article is available over on GitHub.

Generic bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Junit footer banner
Comments are closed on this article!