Aditya Jaroli is a software engineer who believes in explainable solutions, simple design, and clean code. In his blog, he discusses unlocking the power of evaluating quality of a microservice in a simple way using a BDD framework.
In today’s landscape of distributed software, microservices-based system architecture has gained considerable popularity. This type of architecture allows for more flexibility, scalability, and easier maintenance compared to traditional monolithic architectures.
The microservices split one monolithic application into smaller units that are independently delivered and deployed. But this comes with increased complexity, for example it is quite hard to replicate this service mesh locally.
The process of bringing down all the necessary microservices into a local environment for testing is slow, complex, and ineffective. At times, we resort solely to unit test cases for the services, increasing the reliance on manual testing, which is neither scalable nor efficient.
This approach warrants enhancements. Simply relying on unit test cases may lead to faulty services, as only individual classes or functions are validated, neglecting the full picture of how these units interact within the service (as a component). Hence, in the microservices landscape, it’s imperative to develop unit, component, and contract test cases for each service.
Behavior-Driven Development (BDD):
Before we go in depth about component testing, let’s have a brief introduction to behavior- driven development (BDD).
BDD is an Agile methodology where applications are designed around the behaviors a user expects to experience. The behavior of the feature is well described, as is the expected output. This can be passed to the developer as a development goal, and at the end, the same can be used to verify the system before it is pushed to git or shipped. The beauty of BDDs is that they can be written or reviewed by anyone who is familiar with the feature.
In my view, BDDs are a refined version of the TDD approach.
Test Strategies for a Microservice:
Unit Testing: Testing every function/method/piece individually as a single unit. This is the low-level testing of the code in the microservice. It is generally advisable to incorporate a thorough collection of unit test cases during this phase.
Contract Testing: In the context of microservices, contract testing involves verifying the interactions between services by examining their defined contracts.
Component Testing: In the context of microservices, component testing is the examination of individual service or component. In the microservices domain, a component is evaluated by interacting with the APIs it exposes. To focus solely on testing a single component, communication with dependent components or services is simulated (mocked).
In this blog post, I’ll be focusing specifically on component testing.
Let’s Define a Microservice and Its Behavior:
Let’s consider an Extraction service tasked with retrieving data from Azure Blob Storage, performing pre-processing, and subsequently storing it in a PostgreSQL database. This Extraction service relies on the Config service to obtain runtime configurations.
Now let’s define the behavior using BDD framework.
Feature: Testing behavior of Extraction service
As a service owner, I would like to test the endpoints of Extraction service.
Scenario: service health
When user makes get request to /health endpoint
Then the request should be completed with status code 200
Scenario: extraction
When user starts extraction process for the date 2021-01-01
Then the request should be completed with status code 200
And the response should have following properties:extract_date,total_records,status
And in the response, extract_date should be 2021-01-01.
And in the response, total_records should be 1000.
And in response, status should be successful.
And the data should be available in the DB for the date 2021-01-01 and count should be 1000
The file where behaviors are defined is called the feature file. Here, multiple behaviors can be outlined, but for the sake of simplicity, I have only defined two.
Upon reviewing the service diagram and feature file, it becomes evident that to component test this service effectively, we require the following:
- An instance of the Azure Blob Storage (ABS) containing the necessary file(s).
- An instance of the PostgreSQL database with a corresponding table configured to store the data.
- An instance of the Extraction service exposing the endpoints essential for testing the specified behaviors.
- An instance of the Config service to supply the required configuration to the Extraction service.
There are several ways available for performing Component Testing on Extraction service. We could:
- Choose to mock all external communication within the component/service, though this may not give confidence in the quality of the tests.
- Decide to deploy everything in a production-like environment, but this would entail significant costs and maintenance efforts. Moreover, this setup wouldn’t be developer- friendly, as local changes wouldn’t be reflected until the service was deployed. This will increase the feedback cycle time.
- Every component will be installed in a local Docker container for temporary use, ensuring easy disposal once finished. Additionally, all communications to “dependent services” will be mocked within a single Docker container.
- Docker All strategy is scalable, as adding another infrastructure or dependent service is easy with Docker.
- This would also give confidence in the quality, as the services in the Docker container will closely resemble those deployed in the production environment.
- No extra cost and no maintenance required. This is developer friendly, as the local changes can be verified before pushing to CI/CD.
Let’s imagine Docker All strategy with below component diagram.
Each service, whether it’s an infrastructure service or an application service, is deployed as a Docker container within a local Docker network.
The Extraction service is the component under test. The Extractor service exposes two REST endpoints:
- GET — /health
- POST — /extract?order_date=
As part of component testing, we need to test these REST endpoints as per the behavior outlined in the feature file.
In our component testing procedure, we aim to thoroughly evaluate the behavior described in the feature file. Here, a container named “Component Test Service” serves as a client like Postman, facilitating the testing of REST endpoints offered by the Extraction service.
The “Component Test Service” container operates independently of the details of the Extraction Service like language the APIs are developed in, API code, configuration, etc. It solely requires knowledge of the REST endpoint information and the hosting details of the service.
The “Component Test Service” executes the feature file by defining the implementation of each step of the feature file. This makes the “component test service” language agnostic. It can be developed in any technology to call the endpoints of Extraction Service.
Advantages of Docker All Strategy:
- The environment of Service (component under test) is almost same as if it is running in production.
- We can easily mock all the dependent endpoints, and nothing needs to be changed in the Service (component under test). The Service interacts with the dependent service as another Docker container running in the network.
- The component test cases can be executed locally without pushing them to CI/CD.
- The component test cases can also be executed in the CI/CD by running the file available in the source code.
- Issues related to infrastructure or dependent libraries can be identified locally. Docker and related issues for the Service (component under test) can also be identified locally.
- The Component Testing setup can be used locally to debug production bugs.
- Component testing is conducted on the REST APIs provided by the Service, regardless of the technology used to develop the Service.
- Initially, the setup may appear complex, but once it is established, performing component testing on the system becomes straightforward.
I hope this article explains the overall concept around behavioral-driven design!