Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2201.9.0] Add resource mocking content #8874

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,17 @@ intro: Mocking is useful to control the behavior of functions and objects to con

## Mock objects

The `Test` module provides capabilities to mock an object for unit testing. This allows you to control the behaviour of
the object member functions and values of member fields via stubbing or replacing the entire object with a user-defined
equivalent. This feature will help you to test the Ballerina code independently of other modules and external endpoints.
The `Test` module provides capabilities to mock an object for unit testing. This allows you to control the behavior of the methods of an object and values of member fields via stubbing or replacing the entire object with a user-defined equivalent. This feature will help you to test the Ballerina code independently of other modules and external endpoints.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

Mocking objects can be done in two ways :

1. Creating a test double (providing an equivalent object in place of the real object)
2. Stubbing the member function or member variable (specifying the behaviour of the functions and values of the
variables)
2. Stubbing the methods and fields of the object (specifying the behavior of the methods and values of the object fields)
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved


### Create a test double

You can write a custom mock object and substitute it in place of the real object. The custom object should be made
structurally equivalent to the real object via the mocking features in the test module.
You can write a custom mock object and substitute it in place of the real object. The custom object should be made structurally equivalent to the real object via the mocking features in the test module.

***Example:***

Expand Down Expand Up @@ -53,8 +49,7 @@ function getRandomJoke(string name) returns string|error {

Let's write tests for the above `main.bal` file to define a test double for the `clientEndpoint` object.

>**Note:** Only the `get` function is implemented since it is the only function used in the sample. Attempting to call
any other member function of the `clientEndpoint` will result in a runtime error.
>**Note:** Only the `get` method is implemented since it is the only method used in the sample. Attempting to call any other methods of the `clientEndpoint` will result in a runtime error.

***main_test.bal***

Expand Down Expand Up @@ -86,13 +81,14 @@ public function testGetRandomJoke() {
}
```

### Stub member functions and variables of an object
### Stub the methods and fields of an object
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

Instead of creating a test double, you may also choose to create a default mock object and stub the functions to return
a specific value or to do nothing.
#### Stub remote methods or normal methods
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

>**Note:** It is important to ensure that all member functions of the object being tested are properly stubbed.
> If any function is called within the implementation that hasn't been stubbed, the test framework will generate an
Instead of creating a test double, you may also choose to create a default mock object and stub the methods to return a specific value or to do nothing.

>**Note:** It is important to ensure that all methods of the object being tested are properly stubbed.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved
> If any method is called within the implementation that hasn't been stubbed, the test framework will generate an
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved
> error message in the following format:
> `no cases registered for member function '<member_function_name>' of object type '<object_type>'.`
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

Expand Down Expand Up @@ -149,11 +145,11 @@ function getCategoriesResponse() returns string[] {
}
```

#### Stub to return a specific value
##### Stub to return a specific value

***main_test.bal***
This test stubs the behaviour of the `get` function to return a specific value in 2 ways:

This test stubs the behavior of the `get` method to return a specific value in 2 ways:

1. Stubbing to return a specific value in general
2. Stubbing to return a specific value based on the input
Expand Down Expand Up @@ -182,13 +178,13 @@ public function testGetRandomJoke() {
}
```

#### Stub with multiple values to return sequentially for each function call
##### Stub with multiple values to return sequentially for each function call

***main_test.bal***

This test stubs the behaviour of the `get` function to return a specified sequence of values for each `get` function
invocation (i.e., the first call to the `get` function will return the first argument and the second call will return
the second argument).
This test stubs the behavior of the `get` method to return a specified sequence of values for each `get` method invocation (i.e., the first call to the `get` method will return the first argument and the second call will return the second argument).
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

>**Note:** `withArguments` function does not support with `thenReturnSequence`.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

```ballerina
import ballerina/test;
Expand All @@ -211,6 +207,191 @@ public function testGetRandomJoke() {
}
```

##### Stub to do nothing

If a method has an optional or no return type specified, this method can be mocked to do nothing when writing test cases.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

***Example:***

***main.bal***

```ballerina
import ballerina/email;

email:SmtpClient smtpClient = check new ("localhost", "admin","admin");

// This function sends out emails to specified email addresses and returns an error if sending fails.
function sendNotification(string[] emailIds) returns error? {
email:Message msg = {
'from: "builder@abc.com",
subject: "Error Alert ...",
to: emailIds,
body: ""
};
return check smtpClient->sendMessage(msg);
}
```
***main_test.bal***

This test stubs the behavior of the `sendMessage` method to do nothing for testing the `sendNotification` function.

```ballerina
import ballerina/test;
import ballerina/email;

@test:Config {}
function testSendNotification() {
string[] emailIds = ["user1@test.com", "user2@test.com"];

// Create a default mock SMTP client and assign it to the `smtpClient` object.
smtpClient = test:mock(email:SmtpClient);

// Stub to do nothing when the`sendMessage` method is invoked.
test:prepare(smtpClient).when("sendMessage").doNothing();

// Invoke the function to test and verify that no error occurred.
test:assertEquals(sendNotification(emailIds), ());
}
```

#### Stub resource methods
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

Similar to remote methods, resource methods also can be stubbed to return a specific value, a series of values or, to do nothing. To mock a resource method, the resource method name(`get`, `post`, `put`, ...etc) and resource path should be specified. Each path parameter in the resource path should be indicated with a colon(`:`) prefix. Similarly, Rest parameters should be indicated with a double colon(`::`) prefix.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

***Example:***

Let's consider the following client example.

***main.bal***

```ballerina
public type Employee record {|
readonly string id;
string firstName;
string lastName;
string gender;
|};

EmpClient empClient = new ();

public client class EmpClient {
map<Employee> employees;

function init() {
self.employees = {};
}
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

resource function get employees/[string... args]() returns string {
return "Welcome to the Employees API";
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved
}

resource function get employee/[string id]() returns Employee? {
return self.employees[id];
}

resource function get employee/welcome/[string id](string firstName, string lastName) returns string {
return "Welcome " + firstName + " " + lastName + ". your ID is " + id;
}
}
```

##### Stub to return a specific value with a resource method

***main_test.bal***

This test stubs the behavior of the below resource method to return a specific value in 4 ways:
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

```ballerina
resource function get employee/welcome/[string id](string firstName, string lastName) returns string {
return "Welcome " + firstName + " " + lastName + ". your ID is " + id;
}
```

1. Stubbing to return a specific value in general
2. Stubbing to return a specific value based on the path parameter
3. Stubbing to return a specific value based on the method arguments
4. Stubbing to return a specific value based on the path parameter and the method arguments

```ballerina
@test:Config {}
function testWelcomeEmployee() {
empClient = test:mock(EmpClient);
// Stubbing to return a specific value in general
test:prepare(empClient).whenResource("employee/welcome/:id").onMethod("get").thenReturn("Stub_1");

// Stubbing to return a specific value on a specific path parameter
test:prepare(empClient).whenResource("employee/welcome/:id").onMethod("get").withPathParameters({id: ""}).thenReturn("Stub_2");
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

// Stubbing to return a specific value on a specific method arguments
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved
test:prepare(empClient).whenResource("employee/welcome/:id").onMethod("get").withArguments("", "").thenReturn("Stub_3");
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

// Stubbing to return a specific value on a specific path parameter and method arguments
test:prepare(empClient).whenResource("employee/welcome/:id").onMethod("get").withPathParameters({id: ""}).withArguments("", "").thenReturn("Stub_4");
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

// Specific path parameter and method arguments should take precedence over general stubbing
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved
string result = empClient->/employee/welcome/[""].get(firstName = "", lastName = "");
test:assertEquals(result, "Stub_4");

// Specific method arguments should take precedence over general stubbing
result = empClient->/employee/welcome/["emp001"].get(firstName = "", lastName = "");
test:assertEquals(result, "Stub_3");

// Specific path parameter should take precedence over general stubbing
result = empClient->/employee/welcome/[""].get(firstName = "John", lastName = "Kibert");
test:assertEquals(result, "Stub_2");

// General stubbing should be used when no specific stubbing is available
result = empClient->/employee/welcome/["emp001"].get(firstName = "John", lastName = "Kibert");
test:assertEquals(result, "Stub_1");
}
```

##### Stub with multiple values to return sequentially for each function call with a resource method
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

***main_test.bal***

Similar to other object methods, the resource method also can return a value from a sequence of values during method invocation.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

>**Note:** `withArguments` and `withPathParameters` functions do not support with `thenReturnSequence`.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

```ballerina
@test:Config {}
function testGetAllEmployeeById() {
// Create a mock client
empClient = test:mock(EmpClient);
Employee emp1 = {id: "emp001", firstName: "John", lastName: "Doe", gender: "male"};
Employee emp2 = {id: "emp002", firstName: "John", lastName: "Kennedy", gender: "male"};
Employee emp3 = {id: "emp003", firstName: "John", lastName: "Kill", gender: "male"};

// Stub to return the corresponding value for each invocation
test:prepare(empClient).whenResource("employee/:id").onMethod("get").thenReturnSequence(emp1, emp2, emp3);

// Invoke function calls
Employee? result = empClient->/employee/["emp001"].get();
test:assertEquals(result, emp1);
Employee? result1 = empClient->/employee/["emp002"].get();
test:assertEquals(result1, emp2);
Employee? result2 = empClient->/employee/["emp002"].get();
test:assertEquals(result2, emp3);
}
```

##### Stub to do nothing with a resource method

***main_test.bal***

If a resource method has an optional or no return type specified, this method can be mocked to do nothing when writing test cases.
Thevakumar-Luheerathan marked this conversation as resolved.
Show resolved Hide resolved

```ballerina
@test:Config {}
function testGetAllEmployee() {
empClient = test:mock(EmpClient);
test:prepare(empClient).whenResource("employee/:id").doNothing();
Employee? result = empClient->/employee/["emp001"].get();
test:assertEquals(result, ());
}
```

#### Stub a member variable

If a `client` object has a public member variable, it can be stubbed to return a mock value for testing.
Expand Down Expand Up @@ -288,54 +469,6 @@ function testMemberVariable() {
}
```

#### Stub to do nothing

If a function has an optional or no return type specified, this function can be mocked to do nothing when writing
test cases.

***Example:***

***main.bal***

```ballerina
import ballerina/email;

email:SmtpClient smtpClient = check new ("localhost", "admin","admin");

// This function sends out emails to specified email addresses and returns an error if sending failed.
function sendNotification(string[] emailIds) returns error? {
email:Message msg = {
'from: "builder@abc.com",
subject: "Error Alert ...",
to: emailIds,
body: ""
};
return check smtpClient->sendMessage(msg);
}
```
***main_test.bal***

This test stubs the behaviour of the `send` function to do nothing for testing the `sendNotification` function.

```ballerina
import ballerina/test;
import ballerina/email;

@test:Config {}
function testSendNotification() {
string[] emailIds = ["user1@test.com", "user2@test.com"];

// Create a default mock SMTP client and assign it to the `smtpClient` object.
smtpClient = test:mock(email:SmtpClient);

// Stub to do nothing when the`send` function is invoked.
test:prepare(smtpClient).when("sendMessage").doNothing();

// Invoke the function to test and verify that no error occurred.
test:assertEquals(sendNotification(emailIds), ());
}
```

## Mock functions

The Ballerina test framework provides the capability to mock a function. You can easily mock a function in a module that
Expand Down Expand Up @@ -380,11 +513,11 @@ import ballerina/test;
test:MockFunction intAddMockFn = new ();
```

After the initialization, the following options can be used to stub the behaviour of a function written in the module being tested.
After the initialization, the following options can be used to stub the behavior of a function written in the module being tested.

### Stub to return a specific value

This test stubs the behaviour of the `get` function to return a specific value in 2 ways:
This test stubs the behavior of the `get` function to return a specific value in 2 ways:

1. Stubbing to return a specific value in general
2. Stubbing to return a specific value based on the input
Expand All @@ -410,7 +543,7 @@ function testReturn() {

### Stub to invoke another function in place of the real

This test stubs the behaviour of the `intAdd` function to substitute it with a user-defined mock function.
This test stubs the behavior of the `intAdd` function to substitute it with a user-defined mock function.

```ballerina
import ballerina/test;
Expand All @@ -432,7 +565,7 @@ public function mockIntAdd(int a, int b) returns int {
}
```

This test stubs the behaviour of an imported function to substitute it with a user-defined mock function.
This test stubs the behavior of an imported function to substitute it with a user-defined mock function.

```ballerina
import ballerina/test;
Expand Down