Get started with service testing using Ballerina test framework

Fathima Dilhasha
Ballerina Swan Lake Tech Blog
6 min readAug 17, 2021

Testing plays an important role in producing quality software. Your services are not ready to be used by end users unless they are thoroughly tested. This article focuses on writing tests for an HTTP service developed using Ballerina using the Ballerina test framework.

In order to test a Ballerina service, you need to send specific requests to the service using a client and verify the responses by asserting whether the actual responses match with the expected. Your test cases should be comprehensive enough to ensure that all possible requests are gracefully handled by the service whether the requests are in expected form or not.

Use case : A pet store service to add and retrieve pet details

Let’s use a simple pet store service to explain the process of testing a service.

pet store service

This HTTP service implemented using Ballerina Swan Lake does two basic operations i.e. adding a pet to the pet store and retrieving a pet from the pet store.

Testing the service

There are three main steps that need to be followed in order to test a service.

1. Initialising the client

In order to test the pet store service, we need a client which listens on the pet store service endpoint.

As the sample pet store service is defined with the listener host as “localhost” and port as “9090”, we can initialise a client as follows.

http:Client petStoreClient = check new (“http://localhost:9090“);

2. Writing the tests

Then, the above client needs to send requests to the get and post resource functions of the service and verify the responses to check whether the service handles the requests gracefully.

If you are new to Ballerina tests, the files with the test cases for a module need to be added within a ‘tests’ directory. You can refer to the quick start guide on testing for a step by step guide on getting started with tests.

You can refer to the using assertions section of the Ballerina test guide to know more about the functionalities that can be used to validate the service responses.

3. Executing the tests

When you execute Ballerina tests, using the following command, the services defined in the modules will start automatically as part of the testing phase.

$ bal test

You can execute the below command in order to run the tests and generate an HTML code coverage report with code coverage information.

$ bal test --code-coverage --test-report

Following is the generated report which shows the code coverage is 0% because we haven’t added any test cases yet.

Developing the test cases

Similar to a function, a test function should also focus on testing a single case.

An approach you can follow is to write tests for the successful cases(happy path) first. Then, you can manipulate the service calls and come up with test cases to cover unsuccessful cases as well. For this use case, let’s start writing tests targeting each resource function while trying to increase the code coverage for the package.

Testing the get resource function

Let’s write a test case to retrieve an existing pet from the pet store.

We need to pass the get resource function path when calling the client. As ‘http:Client’ is being used, the invocation will return an ‘http:Response’ or an ‘http:ClientError’.

Since this is a successful case, the expectation is an HTTP response with ‘200’ status code and a JSON payload with the pet details. Any other response should fail the test.

In this case, we have used ‘assertEquals’ to verify the status code and response payload.

As a check expression is being used, If there is an ‘http:ClientError’ returned from the get resource, it will be returned from the test function and result in a test failure. So, it’s not necessary to take any action for that erroneous scenario within the test case.

Let’s generate the code coverage report after adding this test case.

You’ll notice that the code coverage percentage has increased to 52.94%. You can view the source file in the report to see the highlighted covered and not covered lines.

Code coverage for the get resource function of the service

Then, let’s add a test case to verify the behaviour when trying to retrieve pet details for a non-existent pet id.

Since this is not a successful case, the expectation is an HTTP response with ‘404’ status code indicating that a pet is not found with the given id. Any other response should fail the test.

The generated code coverage report after adding this test, will show an increased coverage of 64.71%, completely covering the get resource function as follows.

Code coverage for the get resource function of the service

Testing the post resource function

Let’s add a pet that does not exist already. You can pass the pet details as a JSON payload to the service and Ballerina is capable of mapping the details to a record with similar fields. The service will respond with the added ‘Pet’ record and it can be asserted with a JSON to confirm the expected behaviour at the client side.

Ballerina supports seamless data binding, saving the service and client implementation from that hassle.

After adding this test case, the generated code coverage report for the package will show an increased coverage of 88.24%.

Code coverage for the post resource function of the service

Then, let’s write a test to verify the behaviour when adding a pet with an existing pet id.

This test should verify that a pet with an already existing id does not get added replacing the existing pet record. In such a scenario, the service returns an ‘http:MethodNotAllowed’ error with the status code ‘405’. The test case asserts the received status code and error message with the expected.

With this test case, the generated code coverage report will show a 100% coverage now.

Code coverage for the post resource function of the service

Even though the code coverage is complete, we can still add a test case to verify how the pet store service behaves if we send a bad request to the resource functions.

This test sends the pet details in string format even though the resource function expects json. The expected response from the service is an ‘http:BadRequest’ indicating the data binding failure.

Now we have successfully written test cases to validate the requests sent to the post and get resource functions in order to add and retrieve pet details.

If you need to start the HTTP service using a different host name and port for testing, you can configure the values using a ‘Config.toml’ file in the tests path. As the ‘hostEp’ and ‘port’ variables defined in the service.bal are configurable variables, to leverage this functionality you can construct the backend URL for the client as follows.

http:Client petStoreClient = 
check new (“http://” + hostEp + “:” + port.toString());

Also, when expecting the services within the module to start during the test phase, it is important to note that the services will get started only if there are tests defined for that particular module.

It is known that tests are meant to verify the expected behaviour of a service, but it also gives you the perspective of the end user. In the process of writing tests, you will realise better ways to implement service logic taking into consideration even minor details such as error messages.

On a general note, if the service contains any backend calls, you can leverage the mocking capability of the test framework and mock the external endpoints to return curated responses for each test scenario. This allows you to unit test the service in isolation.

Sample service and the tests used in this article can be found at this Git repository.

I hope this post introduces you to the basic building blocks used to test a Ballerina service. Happy testing :)

--

--