Write tests with Ballerina

Yashod Perera
7 min readMar 21, 2024

--

Today we are talking a quick overview of how we do testing in Ballerina and following is the table of content for the day. This will cover everything you need to know in Ballerina testing.

  1. Start with first test
  2. How to structure tests
  3. Test configurations and annotations
  4. How to provide configurations for tests
  5. Generate Test Reports
  6. Data driven testing
  7. Mock clients and functions
Photo by Nihal Demirci Erenay on Unsplash

Let’s get our hand dirty !!!

Start with first test

First you need to create a Ballerina project using bal add <project> . For now let’s use bal add test-example .

Then open the main.bal and let’s add some code.

public function add(int a, int b) returns (int) {
return a + b;
}

Then Let’s create the test file in tests/main_test.bal . Let’s add the first test here.

import ballerina/test;

@test:Config {}
function addTest() {
test:assertEquals(add(1, 3), 4);
}

The @test:Config{} knows that this is a test which must be running. Then you can run tests using bal test . You can find the source repo here.

How to structure tests

In a larger Ballerina project there are multiple modules and test structuring is important.

Unit test must cover only specific units hence module level tests must be resides at module level while application level tests must be resides at root level

You can easily pack module level tests at module level as follows.

.
├── modules/
│ ├── module_one/
│ │ ├── tests/
│ │ │ └── test_file.bal
│ │ ├── file.bal
│ │ └── file2.bal
│ └── module_two/
│ ├── tests/
│ │ └── test_file.bal
│ ├── file.bal
│ └── file2.bal
├── resources
├── tests/
│ └── test_file.bal
└── server.bal

Conclusion — Module level tests must be at module level tests and application level tests must be at application level tests.

Test configurations and annotations

First let’s dive on the configurations which we can used to,

  • Structure test order — this can be done using before , after , dependsOn
  • Group test cases — groups can be used
  • Add data providers — dataProvider
  • Enable/disable test case — enable
@test:Config {
before: intDivTest,
groups: ["math", "div"],
enable: false,
dependsOn: [intMulTest]
}
function intDivTest2() {
test:assertNotEquals(divide(6, 3), 3);
}

Simple example can be found here. You can run specific test cases using

  • bal test --groups group1,group2
  • bal test --tests test1,test2

Then let’s talk about annotations which are essencial which are needed to run before test suite, after each test any many more. Following are the list of annotations which can be used.

  • @test:BeforeSuite
  • @test:AfterSuite
  • @test:BeforeGroup {value: ["group1", "group2"]}
  • @test:AfterGroup {value: ["group1", "group2"]}
  • @test:BeforeEach
  • @test:AfterEach

How to provide configurations for tests

In Ballerina we provide configurable using the Config.toml file and there might be cases where we need to provide different config data to tests. For that specific scenario we can add Config.toml inside the module and that will pick it up. You can check the sample code here.

Generate Test Reports

Visualising the code coverage helps developers, quality assurance engineers to get an overall idea about the test coverage. In Ballerina it provides the facility to generate tests reports and visualise it.

  • bal test --test-report --code-coverage

Data Driven Tests

In testing we might need to execute the same test with different data sets where we need to reuse the same tests. In Ballerina this can be achievable using data providers which provide capability to insert array of data to the tests case.

import ballerina/test;

function dataGen() returns map<[int, int, int]>|error {
map<[int, int, int]> dataSet = {
"test1": [10, 10, 20],
"test2": [5, 5, 10]
};
return dataSet;
}

@test:Config {
dataProvider: dataGen
}
function testAddition(int num1, int num2, int expectedTotal) returns error? {
test:assertEquals(add(num1, num2), expectedTotal, msg = "The sum is not correct");
}

Example can be found here.

Mock clients and functions

We have arrived to the end of our journey. We need to mock clients, classes or functions because we need to test the units regardless of other modules or external endpoints.

  1. Inside the module tests we need to mock external endpoints or dependent module
  2. Inside the application tests we need to mock dependent modules.

Mock a client

It is a common scenario where we need to mock a http client. In Ballerina what we can do is simply create a mock client and say that we are using the mock client instead of actual one as follows.

import ballerina/http;

http:Client clientEndpoint = check new ("https://api.chucknorris.io/jokes/");

type Joke readonly & record {
string value;
};

// This function performs a `get` request to the Chuck Norris API and returns a random joke
// with the name replaced by the provided name or an error if the API invocation fails.
function getRandomJoke(string name) returns string|error {
Joke joke = check clientEndpoint->get("/random");
string replacedText = re `Chuck Norris`.replaceAll(joke.value, name);
return replacedText;
}

In our program we have initialise the clientEndpoint as above, and in the test we can simply say for the clientEndpoint use the mock client as follows. Sample code can be found here.

import ballerina/test;
import ballerina/http;

// An instance of this object can be used as the test double for the `clientEndpoint`.
public client class MockHttpClient {

remote function get(string path, map<string|string[]>? headers = (), http:TargetType targetType = http:Response) returns http:Response|anydata|http:ClientError {
Joke joke = {"value": "Mock When Chuck Norris wants an egg, he cracks open a chicken."};
return joke;
}

}

@test:Config {}
public function testGetRandomJoke() {

// create and assign a test double to the `clientEndpoint` object
clientEndpoint = test:mock(http:Client, new MockHttpClient());

// invoke the function to test
string|error result = getRandomJoke("Sheldon");

// verify that the function returns the mock value after replacing the name
test:assertEquals(result, "Mock When Sheldon wants an egg, he cracks open a chicken.");
}

Wait! How to mock a final client? We cannot re assign a value to a final reference.

Mock a final client

If you are using a final client then we have to do some changes in the code first to test it. Let’s start with the code. First we need to create a function to initialize the client.

import ballerina/http;

http:Client clientEndpoint = check intializeClient();

function intializeClient() returns http:Client|error {
return new ("https://api.chucknorris.io/jokes/");
}

type Joke readonly & record {
string value;
};

// This function performs a `get` request to the Chuck Norris API and returns a random joke
// with the name replaced by the provided name or an error if the API invocation fails.
function getRandomJoke(string name) returns string|error {
Joke joke = check clientEndpoint->get("/random");
string replacedText = re `Chuck Norris`.replaceAll(joke.value, name);
return replacedText;
}

Then we can mock the the initializeClient function as follows.

@test:Mock { functionName: "intializeClient" }
function getMockClient() returns http:Client|error {
return test:mock(http:Client, new MockHttpClient());
}

Example code can be found here.

Stub objects

Stubbing is a common practice which we use in tests. In stubbing we return specific value as the responce when we need it. Let’s get the above example and try to stub the response for /random .

import ballerina/test;
import ballerina/http;


@test:Mock { functionName: "initializeClient" }
function getMockClient() returns http:Client|error {
return test:mock(http:Client);
}

function getMockResponse() returns Joke {
Joke joke = {"value": "When Chuck Norris wants an egg, he cracks open a chicken."};
return joke;
}

@test:Config {}
public function testGetRandomJoke() {

test:prepare(clientEndpoint).when("get").thenReturn(getMockResponse());

// invoke the function to test
string|error result = getRandomJoke("Sheldon");

// verify that the function returns the mock value after replacing the name
test:assertEquals(result, "When Sheldon wants an egg, he cracks open a chicken.");
}

In the above example what we has done was we return a mock response if it calls the clientEndoint with get. Sample code can be found here.

Wait ! Then what if we have several routes? You can use withArguments as follows.

test:prepare(clientEndpoint).when("get").withArguments("/random").thenReturn(getMockResponse());

There are some additional features that Ballerina provides which are you can pass response in a sequence using thenReturnSequence or you can do nothing using doNothing . For more you can read Ballerina documentation.

Mock Function

We can easily mock functions in Ballerina and we can provide some mock values or we can point another function to be called. Let’s go with the first example where we provide some mock values. Here we are using simple add , subtract functions.

public function add(int a, int b) returns int {
return a + b;
}

public function subtract(int a, int b) returns int {
return a -b;
}

Following are sample test cases for the above code.

import ballerina/test;

@test:Mock {functionName: "add"}
test:MockFunction addMockFn = new ();

@test:Config {}
function testReturn() {
// Stub to return the specified value when the `add` is invoked.
test:when(addMockFn).thenReturn(10);

// Stub to return the specified value when the `add` is invoked with the specified arguments.
test:when(addMockFn).withArguments(5, 6).thenReturn(11);

test:assertEquals(add(5, 5), 10, msg = "function mocking failed");
test:assertEquals(add(5, 6), 11, msg = "function mocking with arguments failed");
}

If you need to mock a function from different module then you need to follow the following standard.

@test:Mock {
moduleName: "module"
functionName: "add"
}
test:MockFunction addMockFn = new ();

Final thing we need to know is you can simple assign a function for the mock as follows.

import ballerina/test;

@test:Mock {functionName: "add"}
test:MockFunction addMockFn = new ();

@test:Config {}
function testCall() {
// Stub to call another function when `intAdd` is called.
test:when(addMockFn).call("mockadd");

test:assertEquals(addValues(11, 6), 5, msg = "function mocking failed");
}

// The mock function to be used in place of the `intAdd` function
public function mockadd(int a, int b) returns int {
return a - b;
}

Please note that most of the examples has been extracted from the Ballerina test documentation and additional information can be found here.

Reference

Hopefully this is helpful.

If you have found this helpful please hit that 👏 and share it on social media :).

--

--

Yashod Perera

Technical Writer | Tech Enthusiast | Open source contributor