Skip to content

nirmal070125/my_integration_project

Repository files navigation

A Novel Approach To Test Your Integration Project

Introduction

In a typical integration project, as developers you would like to continuously enhance your integration logic with the user feedback or with your new realizations about the use cases. How can you do it confidently? .. well the answer is integration tests. You need to write your integration tests to verify and validate the logic you have implemented. Traditionally, developers used testing tools such as SOAP UI, Jmeter etc. or used programming languages such as C, Python, Java etc. SOAP UI, Jmeter are not flexible as they are graphical UI driven, hard to maintain as the output is set of scripts, not developer friendly as you need to learn specifics on using those tools (required dedicated QA teams to build those scripts). Writing integration tests using programming languages are never easy as you need to write lots of code to get a simple test done. Is there a solution? ...

Ballerina is a simple programming language whose syntax and platform address the hard problems of integration. It is a general purpose, concurrent, transactional, statically and strongly typed programming language with both textual and graphical syntaxes. Its specialization is integration - it brings fundamental concepts, ideas and tools of distributed system integration into the language and offers a type safe, concurrent environment to implement such applications. These include distributed transactions, reliable messaging, stream processing, workflows and container management platforms [1].

Ballerina has a built-in test framework named Testerina [2]. Testerina enables developers to write testable code. The test framework provides a set of building blocks to help write tests and a set of tools to help test. Developers and testers can cover multiple levels of the test pyramid including unit testing, integration testing and end to end testing with the building blocks the framework provides. It provides the flexibility to programmers and testers to build intelligent tests that suites the domain and application needs. But Testerina is to test Ballerina code, right? How can it test my integration logic? ...

I am one of the designers of Testerina and I believe we can use Testerina as a general test framework to implement integration tests for your integration projects. Testerina was designed in a way that you can leverage the full set of features of Ballerina even to write test cases. For an example, in order to provide a value set for a particular test case, you have the ability to implement another Ballerina function which returns an array of arrays. This will allow you to build your value set within the code itself by using random variables etc. instead of defining value set in a config file. Hence, I do not foresee any issue of using Testerina to test your general integration logic which is implemented using any framework or language. This is a proof of this concept of using Ballerina's Test Framework as a general integration test framework.

alt tag

Proof Of Concept

I am using following technologies, tools, artefacts in this PoC.

I used Azure Cloud Container Service and spin up a new container instance for WSO2 EI - Integration profile. You have to open up 9443, 8243 and 8280 ports and expose a public IP, in order to access WSO2 EI management console and invoke services.

Another docker container was used to deploy WSO2 EI - Msf4j profile and deployed the Hospital Service 2.0.0 jar file (https://github.com/wso2-docs/WSO2_EI/blob/master/Back-End-Service/Hospital-Service-2.0.0.jar).

Project structure of the source repository;

File/Directory Description
IntegrationTests Ballerina based integration tests to test the API.
SampleServices An ESB Project - which contains the API definition.
SampleServicesCompositeApplicationStaging Composite application project for staging environment.
SampleServicesConnectorExporter Connector project.
SampleServicesRegistry Registry resources project common for all the environments.
SampleServicesRegistryStaging Registry resources project for staging environment.
src/main/resources/security Contains the keystore file for the WSO2 EI, in order to deploy the Application.
.travis.yml Travis CI configuration file.
pom.xml Maven POM file for the maven multi-module project.

As you can see there's a registry resource project for each environment which contains the environment specific configuration such as endpoint definitions for backend services. You would have another registry resource project for the production environment, in a real project.

In WSO2 EI, there's a concept of Composite Application and that's the deployable artefact for a WSO2 EI server. You will have another such a Composite Application project for production environment and that should follow the same naming convention i.e. SampleServicesCompositeApplication<ENV_NAME>

IntegrationTests is a Ballerina project and it has a tests sub folder where you place your Ballerina files which contains your integration tests.

In .travis.yml of this repository, you can find following section;

  ######################################################
  # Staging environment ################################
  ######################################################

  - export ENV=Staging
  # build the artefacts and deploy in staging server
  - mvn clean install -q
  - cd SampleServicesCompositeApplication$ENV
  - mvn clean deploy -Dmaven.deploy.skip=true -Dmaven.car.deploy.skip=false
  - cd ../
  - sleep 10
  # run the integration tests
  - cd IntegrationTests
  - ballerina test

Hope this is quite self-explanatory, where it first sets the environment name, then build the project, navigate to the composite application project, deploy the application to the staging server, wait few seconds for the application to be deployed and then run the automated tests. But where do you define the server urls? Those are defined in the pom.xml file of the composite application project as follows;

       <plugin>
        <groupId>org.wso2.maven</groupId>
        <artifactId>maven-car-deploy-plugin</artifactId>
        <version>1.1.1</version>
        <extensions>true</extensions>
        <configuration>
          <carbonServers>
            <CarbonServer>
              <trustStorePath>../src/main/resources/security/wso2carbon.jks</trustStorePath>
              <trustStorePassword>wso2carbon</trustStorePassword>
              <trustStoreType>JKS</trustStoreType>
              <serverUrl>${env.STAGING_EI}</serverUrl>
              <userName>admin</userName>
              <password>admin</password>
              <operation>deploy</operation>
            </CarbonServer>
          </carbonServers>
        </configuration>
      </plugin>

Still it has ${env.STAGING_EI}? Yes, correct. I don't want to expose my server URL to public, so I have encrypted the server url using Travis encrypt [3] feature and the encrypted values are at the bottom of the .travis.yml file's env.global section.

Ballerina Based Test Case

So, let's go through the HealthCareAPITests.bal. I have written only one test case right now, but I hope this will give you an idea on how you could leverage Testerina in your testing. Let's analyze the test code;

import ballerina/http;
import ballerina/test;
import ballerina/config;
import ballerina/io;

These are set of imports that I needed in this test case and the wonderful news is you don't need to import any libraries, write POM files etc. as all these libraries are in-built to the Ballerina language.

string env = config:getAsString("ENV", default = "Dev");
string eiEP = config:getAsString(env +"_EI_SERVICE", default = "http://localhost:8280");
http:Client healthAPIEP  = new(eiEP + "/healthcare");

Ballerina config library help you to read environment variables, config files etc. If you can recall in the .travis.yml file, I have set the "ENV" environment variable and here I'm retrieving that value from within my test code.

In the test case, I need to call the API hosted in this environment and in order to do that I need the server url. Again, this is encrypted using Travis encrypt and the encrypted value is stored in the .travis.yml file.

Finally, we are instantiating a new HTTP client to the healthcare API using the server url.

int currentApptNumber = -1;

@test:BeforeEach
function beforeEachTest() { 
    currentApptNumber = -1;
}

currentApptNumber is a global variable and an integer to keep track of the ongoing appointment number. This is used within the test code.

@test:BeforeEach is a annotation from the test framework and as the name implies the function will get executed before each and every test case. I am resetting the value of currentApptNumber in that function.

@test:Config {
    dataProvider: "surgery_reserve_same_doctor_appointments_data_gen"
}
function surgery_reserve_same_doctor_appointments_test(json aReq, json aRes) returns error?{

Then, I define my first test case. In Testerina, a test case is recognized by @test:config annotation. Within the annotation, I am defining a dataProvider which is a name of another Ballerina function that returns a json array of arrays. These 2 dimensional json arrays will be fed into this function recursively by the test framework. So, eventhough I have only one test case here, if I provide two json arrays, my test case will run twice against the two json arrays. This dataProvider feature is a elegant way to generate different combinations of data for testing purposes.

http:Response | error res =  healthAPIEP -> post("/categories/surgery/reserve", aReq);

In this line, I am doing a HTTP POST request using the input json; aReq. This is a network call (denoted by -> ) and it returns a HTTP response or an error.

if (res is error) {
    test:assertFail(msg = "Error: "+ res.reason());
    return;
} 

If the returned response is an error, we should fail the test case as we couldn't proceed with assertions. Testerina provides assertFail operation and we can set msg to log the reason for failure.

http:Response response = res;
// assert the status code
test:assertEquals(res.statusCode, 200, msg="Unexpected status code!");

This is how you could assert the status code of the returned HTTP response. Now, we can see how we can assert different attributes of the response.

// assert the payload
json payload = check res.getJsonPayload();

check is a special operation in Ballerina where you can lift the error. As I'm pretty sure that my service returns a JSON, I'm neglecting the error here and assigning the payload to a json variable. In Ballerina, json is a type in the language which makes you parse json objects pretty easily.

if (currentApptNumber == -1) {
    currentApptNumber = untaint check int.convert(payload.appointmentNo);
} else {
    // test the appointment number
    currentApptNumber = currentApptNumber + 1;
    test:assertEquals(payload.appointmentNo, currentApptNumber, 
    msg = "Appointment numbers are not generated correctly!");
}

payload.appointmentNo will give us a json which has the value of appointmentNo field of the payload json. Since, I am sure that this field is an integer, I'm converting the json object to and integer using int.convert. Then, I'm using check to lift the error and marking the value as untaint. This is another feature in Ballerina language where it is designed to ensure that programs written with Ballerina are inherently secure. Refer [4] for more information.

test:assertEquals can compare any two similar data types. In this case, I am comparing two json objects for their equality.

// test the discount
float price = check float.convert(payload.actualFee);
int discount = check int.convert(payload.discount);
float actualPrice = check float.convert(payload.discounted);
test:assertEquals(actualPrice, price * (100 - discount)/100, msg ="Discount calculation wrong!");

Here, I am verifying whether the discounting logic is implemented correctly. I think that covers the important set of code in this test case. Refer to [2] on other features of Testerina.

In Action

Checkout the Travis CI build for this repository at https://travis-ci.com/nirmal070125/my_integration_project/builds/101243343

Following is a screenshot of the final set of lines of the build output;

alt tag

Future Improvements

I am thinking of following future improvements to this PoC;

  • Testing a SOAP service
  • Performing a load test with concurrent users
  • Replacing Travis CI with Ballerina

References

[1] https://ballerina.io/learn/

[2] https://ballerina.io/learn/how-to-test-ballerina-code/

[3] https://docs.travis-ci.com/user/encryption-keys/

[4] https://ballerina.io/learn/by-example/taint-checking.html

About

Novel Approach To Test Your Integration Project

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published