BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Developing Microservices with Behavior Driven Development and Interface Oriented Design

Developing Microservices with Behavior Driven Development and Interface Oriented Design

Leia em Português

Key Takeaways

  • Behavior Driven Development for microservices concentrates on collaboration of the triad – consumer developers, producer developers, and testers
  • Create well-defined contracts for microservice interfaces using Interface Oriented Design
  • Microservices typically require test doubles to speed up testing of other microservices.
  • Tests should be implementation independent  
  • Create tests that check failures are appropriately handled

Microservices are relied upon by other microservices and by entire applications.  This dependence requires services that are well defined and well tested.  These goals can be achieved with behaviors and interface contracts specified by tests.  With Behavior Driven Development (BDD), a service’s functionality is described by tests that concentrate on the operations to be performed rather than the syntax of these operations such as JSON or XML.    Automating these tests typically requires test doubles for other microservices whose behavior is specified by their own BDD tests.  Interface Oriented Design (IOD) includes other contractual obligations of a microservice, such as limitations on resource usage, throughput, and error reporting.    Together BDD and IOD help describe a service’s behavior so that consumers can easily understand and rely upon it.       

Context

BDD involves the triad – the three perspectives of the customer, of the developer, and of the tester.  It’s usually applied for the external behavior of an application.   Since microservices are internal, the customer perspective is that of the internal consumer, that is, the parts of the implementation (e.g. other microservices) which uses the service.   So the triad collaboration is between the consumer developers, the microservice developers, and the testers.   

Behavior is often expressed in a Given-When-Then form, e.g.  Given a particular state, When an action or event occurs, Then the state changes and/or an output occurs.  Stateless behavior, as business rules and calculations, just shows the transformation from input to output.  

Interface Oriented Design focuses on the Design Patterns principle “Design to interfaces, not implementations”.   A consumer entity should be written against the interface that a producer microservice exposes, not to its internal implementation.    These interfaces should be well defined, including how they respond if they are unable to perform their responsibilities.   Domain Driven Design (DDD) can help define the terms involved in the behavior and the interface.  

Microservices can either be synchronous where a consumer calls another producer microservice directly and awaits the result or asynchronous where the service responds to a message that the consumer has placed on a queue.   This article’s examples will be on synchronous services.  

Context of the example 

A service provides a cohesive set of related operations.   This example in an ordering application is a service that computes the discount for a customer placing an order.   

An outline of the behavior of this service could be:

Get discount for a customer 
Given these inputs: 
     Customer category 
     Order Amount 
Then service outputs 
     Discount Amount 

The service may compute the result using an implementation in code, a local database, or contacting other services.   We’ll take a look at that later.    

The service may use JSON or XML as the underlying communication protocol.    However, specifying the behavior of the service in an implementation independent way helps to separate the operations from the syntax.   

What is the behavior? 

With BDD, one can start with sample data to get an understanding of the desired behavior.  The triad may come up with:   

Customer Category Order Amount Discount Amount?
Good 100.00 USD 1.00 USD
Excellent 100.00 USD 2.00 USD

The first two columns are the input to the service and the column on the right is the output from the service. 

The sample identifies domain terms which might need further descriptions of their behavior, such as allowable values.   The triad could agree upon the following terms.   The implied contract for the service is that it should return the proper value if the inputs fall within these allowable values.  

Customer Category
Good
Excellent
Super Excellent
Currency
USD
EUR
CAD

Behavior, particularly with microservices, often include responses that indicate failure to perform the operation.  Defining the potential failures helps the consumer determine what it needs to handle.   Consumers may use standard libraries (e.g. Netflix’s Hystrix) to deal with some of these failures.   Some potential failures might be:  

Failure
Syntax not valid
Timeout on dependent services
Parameter value invalid

Failures may be expressed as numeric values or symbolic values in the communication protocol.   Using meaningful names in the BDD helps to emphasize the semantics of the failure, rather than the failure syntax.    For example, If the value passed as the category does not appear on the list of valid values, then the service would return a failure indicator that corresponds to “Parameter value invalid”.     This might be represented in the underlying service by a “400 – Bad Request” return with a payload of “Parameter value invalid”. 

Alternatively, one could define a default value to return (e.g. 0) if any of the parameters were invalid.  The service should have the responsibility to log this issue, so that its ramifications can be analyzed.   

BDD service tests can form a context for the unit tests of entities (e.g. classes) that comprise the service.   Through the design process, responsibilities for passing the BDD tests are assigned to the classes and methods.   The unit tests specify these responsibilities.   

Test Doubles 

A consumer of a service often requires a test double for services it calls.  In particular, services that are slow, expensive, or random need test doubles.  If the behavior of the discount service never changed, then the production discount service might be used in testing the consumer.   However, changes are inevitable, so that a test double is typically required.  

The test double could be one that always returns the same values, e.g. 

Customer Category Order Amount Discount Amount?
Good 100.00 USD 1.00 USD
Excellent 100.00 USD 2.00 USD

The consumer’s tests could rely on these values.  In this example, constant behavior might be sufficient.   However, for other tests, it may be preferable that the test sets up the response of the test double.   

The consumer test would set up the discount test double to respond with the given value when the input is presented.   For example:    

Customer Category Order Amount Discount Amount?
Good 100.00 USD 1.00 USD

Alternatively, the discount test double could simply respond with the amount, regardless of what input was presented to it.  

Let’s see how this test double might fit into a larger scenario.  Here’s the behavior for an order that includes both a discount and a tax.   The tax is computed by a microservice that is similar to the discount.   

Given a customer: 

Customer Category Location
Good North Carolina

And discount is setup as:

Customer Category Order Amount Discount Amount?
Good 100.00 USD 1.00 USD

And tax is setup as: 

Location Amount Tax?
North Carolina 99.00USD 6.60 USD

When the customer places an order with: 

Order Amount
100.00 USD

Then the amounts on order are: 

Order Amount Discount Amount after Discount Tax Total Due
100 USD 1.00 USD 99.00 USD 6.60 USD 105.60 USD

Stateful Services 

If the discount service relied on a local database for information on how to compute the discount, then the database contents represents a state of the service.  How the service’s state changes in response to updates to the data should be documented.   For example, suppose the service relied on the following data:  

Customer Category Threshold Level Discount percentage
Good 100.00 USD 1%
Excellent 50.00 USD 2%

There will be some other aspect of the service that permits the update of this data.  The update could be arranged so that individual elements are updateable or that the entire table is updated at once. Here’s an example of a behavioral test for individual update:  

Given the current data:  

Customer Category Threshold Level Discount Percentage
Good 100.00 USD 1%
Excellent 50.00 USD 2%

When an element is updated:

Customer Category Threshold Level Discount Percentage
Excellent 50.00 USD 3.5%

Then the updated data is: 

Customer Category Threshold Level Discount Percentage
Good 100.00 USD 1%
Excellent 50.00 USD 3.5%

One could also check that the updated data is used to compute the discount: 

Customer Category Threshold Level Discount Amount?
Excellent 100.00 USD 3.50 USD

The discount service could have a local persistent storage mechanism to save the data in the previous example.   However, it could depend on another persistency service to maintain this data. If that were the case, the tests in the previous section would be applied to that service.   Every dependency can bring up another issue. What should be the behavior of a service if its dependencies are unavailable?   For the discount service, should it indicate a failure or just return a default value?   Sometimes a global failure policy can provide the answers, but often the decision relies on the context of the service.    

Test Formulation and Automation

Once the behavior for a microservice has been agreed upon, it can be formulated into automatable tests.   There are several microservice testing frameworks such as PACT or Karate.  Alternatively, you can use BDD frameworks such as Cucumber or FIT.   The test implementation, e.g. the Cucumber step definitions, uses microservice libraries to execute the request/response.   Additional environmental information can be provided as part of the scenario or as background.  For example, a Cucumber feature file could include the following.  There could be several variations, depending on your testing conventions.    

Scenario: Compute discount for an order amount 
Given setup is: 
| URL    | http://myrestservice.com  |
When discount computed with: 
| Method | GET               |
| Path   | discount          |
| Version| 1                 |
Then results for each instance are:
| Customer Category | Order Amount | Discount Amount? |
| Good              | 100.00 USD   | 1.00 USD         |
| Excellent         | 100.00 USD   | 2.00 USD         | 

The values in the first two columns could be transferred into whatever is your calling convention, such as into query parameters. The result in the body should match the third column.   If the query names and values are the column names and values, this decreases the impedance between the test and the implementation 

For reusability, step definitions could be written for any service that computes a calculation or determines a business rule result so that a common parsing library could be used.  In the example above, a convention such as the “?” (as in Discount Amount above) helps distinguish for the parser what is input and what is output.      

The tests should also include tests for failure modes, such as:

Then results for each instance are:
| Customer Category | Order Amount | Discount Amount? | Result                  |
| Good              | 100.00 USD   | 1.00 USD         | OK                      |
| Not so Good       | 100.00 USD   | 2.00 USD         | Parameter value invalid |
| Excellent         | 100.00 ZZZ   | 2.00 USD         | Parameter value invalid |

Conclusion

Designing microservices with BDD concentrates on the semantics of the operations and not the underlying syntax of the implementation.   Following the IOD guideline that services are responsible for performing their operation and notifying someone (consumers or a logger) of issues, helps to delineate the responsibilities for reaction to failures.  Having well defined services makes it much easier to make them work together to provide the desired external behavior.   

About the Author

Ken Pugh helps companies evolve into lean-agile technical organizations through training and coaching. His special interests are in creating high quality systems with Acceptance Test-Driven Development / Behavior Driven Development, accelerating DevOps by collaboration,  and using lean principles to deliver business value quickly.  He has written several software development books including the 2006 Jolt Award winner Prefactoring and his latest: Lean-Agile Acceptance Test-Driven Development: Better Software Through Collaboration. Ken has helped clients from London to Boston to Sydney to Beijing to Hyderabad. He is the co-creator of the SAFe® Agile Software Engineering course. You can see all his services and contact him at ken@kenpugh.com.  

Rate this Article

Adoption
Style

BT