Skip to main content

4 posts tagged with "clean code"

View All Tags

Designing and Implementing Microservices with DDD and Hexagonal Architecture

· 12 min read
Reda Jaifar
Lead Developer

Use case

In this phase, we begin by collecting requirements from a business perspective to ensure a thorough understanding of the objectives. Once these requirements are established, we then investigate how architectural patterns and software development methodologies can be utilized to construct the product effectively.

Currently, local grocery stores must either visit large distribution companies or contact them individually to inquire about pricing, stock availability, and delivery schedules when placing orders. This process is time-consuming and inefficient for grocery managers. To address this challenge, we propose developing a platform that enables managers to easily check prices, view stock levels, place orders, and track deliveries in one streamlined solution.

Personas and User Journey

To develop a system that effectively tackles the relevant challenges and provides significant value, it is imperative that every aspect of the system is designed with users in focus. This makes it vital to thoroughly understand their frustrations, motivations, and expectations regarding the new product we are creating

  • Grocery Manager
    • Profile: Typically has a bachelor's degree.
    • Frustrations: Running out of stock, lack of visibility over deliveries, price comparison is a time-consuming task.
    • Motivations: Save management time, focus on customer relationships and service quality, ensure stock levels are always adequate.

Architecture & Design

In the previous section, we presented a brief overview of the business domain, requirements, and one of the system's future users. At this stage, we need to make architectural and design decisions.

Architectural Style

While the brainstorming outputs for this example are somewhat limited, we can easily envision various services, such as Order Service, Provider Service, Delivery Service, and Product Service. Now, suppose we want to develop all these services simultaneously, with different teams specializing in distinct technology stacks. It is essential that these services remain decoupled from one another, ensuring that a change in the design of one service does not affect the others.

Regarding deployment, we aim to deploy the Product Service before the others to deliver value as quickly as possible. This necessitates that the services be independently deployable and scalable.

By organizing our product around services, we can enhance fault tolerance. For example, if the Delivery Service is unavailable, users should still be able to access other services. For these reasons, we believe that a microservices architecture is the most suitable architectural style for this project.

Regarding deployment, we aim to deploy the Product Service before the others to deliver value as quickly as possible. This requires that services be independently deployable and scalable.

Domain Design

The complexity of the business domain, the rules, and the number of services (system components) lead us to consider Domain-Driven Design (DDD). So, what is DDD?

info

DDD, as described in the excellent book Domain-Driven Design by Eric Evans (Addison-Wesley Professional, 2003), is an approach to building complex software applications that is centered around developing an object-oriented domain model.
A domain model captures knowledge about a domain in a form that can be used to solve problems within that domain.

In traditional object-oriented design, a domain model is a collection of interconnected classes. For example:

Figure 1

Figure 1: Object Oriented Domain Model

With this design, operations such as loading or deleting an Order object encompass more than just the Order itself; they also involve related data, such as order items and delivery details. The absence of clear boundaries complicates updates, as business rules, imagine a business logic such as "minimum order amounts" must be enforced meticulously to preserve invariants

This is where DDD can help, by using Aggregates

An aggregate is a cluster of domain objects within a boundary that can be treated as a unit

Figure 2 shows a simplified version of the domain model aggregates. Designing domain model using the DDD Aggregate pattern recommand that aggregates match a set of rules: 1. Reference only the aggregate root 2. Inter-aggregate references must use primary keys 3. One transaction creates or updates one aggregate

Figure 2

Figure 2: Domain Model Aggregates Simplified

Implementation

Now that we have designed the domain model aggregates and the achitecture implementation view Microservices, let's dive into the architecture logical view Hexagonal Architecture , please refer to this post to learn about Architecture Implementation view, Hexagonal option and why we adopt it

I'm going to use Kotlin as programming language and maven as a build tool

Pilot feature

Bootstrap the project

Let's consider the following feature: place order

As a grocery manager, I need to place an order for a product so I can provision the stock Constraint 1: The minimum order quantity is 20.

Using maven build tool, let's create a project for kotlin.

  1. create a maven project using the following cmd:
    mvn archetype:generate \
-DgroupId=com.algodema \
-DartifactId=grocery-marketplace-order-service \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=false
  1. convert the project to kotlin by replacing the src/main/java and src/test/java directories with src/main/kotlin
  2. modify pom.xml for Kotlin, JUnit, and AspectJ
  3. checkout the project on github: (https://github.com/algodema/microservices-labs/tree/main/grocery-marketplace-order-service)

Implement the place order feature using TDD

What about starting with writing a test scenario for our uese case using TDD (Test Driven Development), this approach help me immediately implement business logic, in this case ensure no order will be created with quantity less than 20 unit.

    @Test
fun `should not place order with quantity lower than 20`() {

val productId: ProductId = ProductId(UUID.randomUUID())
val providerId: ProviderId = ProviderId(UUID.randomUUID())
val quantity: Int = 16

val orderCreated = placeOrder.invoke(productId, providerId, quantity)

Assertions.assertEquals(orderCreated.state, OrderState.PLACED)
}

I started by creating "PlaceOrderFeatureTest" test class, then writing my first test as we see in the snippet above. If we look at the project structure in figure below: Figure 3

note

Within the domain package, I created two sub-packages: features and models. The models sub-package includes representations of key domain entities, such as Order, OrderId, ProductId, and OrderState. Meanwhile, the features sub-package contains feature-specific classes, with PlaceOrderFeature being the current implementation. It's important to note that in the models package, we organize classes according to their respective business domains.

The approach involves declaring instances of productId, providerId, and placeOrder prior to the existence of their respective classes. Subsequently, these classes are developed and organized into packages in accordance with the principles of hexagonal architecture, which positions business logic at the core, as represented by the domain package.

We continue writing tests, they should be fixed and failed as we implement the business requirements in our place order feature.

Infrastructure implementation

Now that we created our first feature, we would like to expose it through a REST API endpoint, but also persist the created Order in a storage, for the purpose of this tutorial, we will implement a in-memory persistence.

The hexagonal architecture defines ports and adapters as interfaces and implementations consequentially used to make the domain interacting and connecting with other components of the application such as (persistency, api, messaging, ...)

note

A port defines a set of operations that facilitate interaction between business logic and external systems. In our Kotlin example, these ports are represented by Java/Kotlin interfaces. An adapter manages requests from external sources or from the business logic itself by invoking external applications or services, such as databases or message brokers. Both ports and adapters can be categorized as inbound or outbound to distinguish between requests directed toward the business logic and those initiated by it.

Ports destination packages

Ports will reside in the same root package as domain because they are integrated part of it. For our example: com.algodema.grocery.markeplace.domain.ports As mentioned before, we separate them into 2 distinct sub packages:

  • com.algodema.grocery.markeplace.domain.ports.inbound
  • com.algodema.grocery.markeplace.domain.ports.outbound

Where adapters reside in the infrascture root package that we create to group all infrastcutures adapters such as:

  • REST API controllers classes
  • InMemory, Postgres or any other Repository implementations that serve to persist data.
  • External Systems integration such as SAPClient for example.

Let's create the follwing ports and adapters:

  1. OrderRepository as an outbond port.
  2. PlaceOrder as inbound port.
  3. OrdersApi as inbound adapter that will use PlaceOrder port to expose the feature as REST API endpoint.
  4. InMemoryOrderRepostory as an outbound adapter that will implement the OrderRepository port interface.

Below we created the InMemoryOrderRepository class that implements the domain port OrderRepository interface, Note also that we annotate this class with the Spring framework @Repository in order to make it discoverable by Spring IoC container. Remember that we use Spring at the infrastcuture level without any coupling with the domain.

    package com.algodema.grocery.marketplace.orderservice.infrastructure.spi

import com.algodema.grocery.marketplace.orderservice.domain.models.order.Order
import com.algodema.grocery.marketplace.orderservice.domain.ports.outbound.OrderRepository
import org.springframework.stereotype.Repository

@Repository
open class InMemoryOrderRepository: OrderRepository {
override fun save(order: Order): Order {
throw NotImplementedError("not yet implemented")
}
}

Next, we will introduce the Spring framework at the infrastructure layer to create a REST API. We rely on the Spring framework's dependency injection to make our component connections decoupled. Using Dependency Injection, the place order feature will hold an instance of OrderRepository to save the created order, and at the infrastructure's API adapter, the REST Controller will hold instances of our features by dependency injection as well.

note

This is where Hexagonal Architecture shines. We can replace Spring by any other framework for exposing REST APIs or handling persistence without modifying the code within our domain. This decoupling keeps the domain safe, adaptable, and maintainable, allowing us to change or add new business rules independently of the infrastructure. For example, if we decide to switch to the Quarkus framework because it is better suited for cloud-native environments, the domain remains completely unaffected.

To enable Spring to identify our features for dependency injection, we will create a new root package designated as ddd. This package will encompass the necessary annotations:

  1. Feature Annotation: marks our features classes
    package com.algodema.grocery.marketplace.orderservice.ddd

@Retention(AnnotationRetention.RUNTIME)
annotation class Feature()

After creation, we utilize the Feature annotation to designate our place order functionality accordingly.

    @Feature
class PlaceOrder(private val repository: OrderRepository) : PlaceOrder {
// ...
}
tip

As previously noted, we will be utilizing Spring Boot for this project. Therefore, it is essential to incorporate the Spring Boot and Spring Web dependencies into our project, as well as to include the Spring Boot Maven plugin within the Maven build plugins.

Let's create the OrdersApi in the infrastructure package under the sub package api, as follow:

    package com.algodema.grocery.marketplace.orderservice.infrastructure.api

import com.algodema.grocery.marketplace.orderservice.domain.features.PlaceOrder
import com.algodema.grocery.marketplace.orderservice.domain.models.order.Order
import com.algodema.grocery.marketplace.orderservice.domain.models.product.ProductId
import com.algodema.grocery.marketplace.orderservice.domain.models.provider.ProviderId
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/orders")
class OrderServiceApi(private val placeOrder: PlaceOrder) {

@PostMapping
fun placeOrder(@RequestBody placeOrderRequest: PlaceOrderRequest): Order {

val productId: ProductId = ProductId.from(placeOrderRequest.productId)
val providerId: ProviderId = ProviderId.from(placeOrderRequest.providerId)
val quantity: Int = placeOrderRequest.quantity

return placeOrder.invoke(productId, providerId, quantity)
}

}

We now need to configure Spring to recognize our annotated features, enabling them to be loaded into its bean container. To achieve this, we will create a configuration class within a subpackage named config under the infrastructure package. Below is our configuration class:

    package com.algodema.grocery.marketplace.orderservice.infrastructure.config

import com.algodema.grocery.marketplace.orderservice.ddd.Feature
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.FilterType

@Configuration
@ComponentScan(
basePackages = ["com.algodema.grocery.marketplace.orderservice"],
includeFilters = [ComponentScan.Filter(
type = FilterType.ANNOTATION,
value = [Feature::class]
)]
)
open class DomainInjectionConfig

The final step is to transform our application's entry point class into a Spring Boot application as follows:

    package com.algodema.grocery.marketplace.orderservice

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication


@SpringBootApplication
open class App

fun main(args: Array<String>) {
runApplication<App>(*args)
}

You may have noticed the presence of the keyword "open" preceding the classes App, DomainInjectionConfig, and InMemoryOrderRepository. Here is the rationale behind this choice:

note

In Kotlin, classes are final by default, meaning they cannot be subclassed unless explicitly marked as open. This is different from languages like Java, where classes are open for inheritance by default unless marked as final.

In Spring Boot (and Spring Framework in general), many of its features rely on proxy-based mechanisms. These mechanisms involve subclassing beans to apply aspects like transaction management, security, lazy initialization, and other cross-cutting concerns. For these proxy-based features to work, Spring needs to be able to create subclasses of certain beans, which means the classes need to be open.

For the purpose of this exercice we decided to use the open modifier to make our classes annotated with Spring not final as we have few classes, but for large application we can use the All-open compiler plugin instead of preceeding each classe required to be open with the open keyword.

Finally, let's run the application either using your IDE such as Intellij Idea or from command line using maven as follow:

mvn spring-boot:run

Below a screenshot of the place order request response overview:

Screenshot

Conclusion

This post has walked you through the entire process of building a fully functional Microservice, from design to implementation, using DDD and Hexagonal Architecture. My goal was to share knowledge and experiences regarding the methodology, architecture, and patterns needed to create a maintainable, extensible, and deployable Microservice. However, delivering a production-ready product requires addressing more advanced aspects. Below is a non-exhaustive list of such considerations:

  • Api Errors Handling
  • Application security
  • Database persistency
  • Api documentation
  • Env variable config

Keep in mind that no single pattern, architectural style, or programming language suits all software product requirements. It's important to focus on understanding and defining the requirements, parameters, and challenges to make the most informed and effective decisions


References:


Clean code: Write The Code You Want To Read (Part 2)

· 5 min read
Reda Jaifar
Lead Developer

author photo source

Functions

Functions constitute a centric component in the recent software programs, the reason why we should care a lot about all of their aspects from naming, length, composition, arguments and error handling.

Small

Yes "small" is the main rule a function should comply with, so it tell us what it does exactly because a function should do one thing, do it well and only.

To keep the function also short, [if, else, while, etc ...] statements should be only one line, and probably this line is function call:

fun bookTrain(bookingRequest: BookingRequest): Booking {
validBookingRequest(bookingRequest)
val booking = Booking.from(bookingRequest)
booking.status(BookingStatus.PENDING)
return booking
}

Single abstraction level

Or the principle of "Doing one thing", the idea is not about writing function with single line of code, or one step but writing it with the restriction to cover only one computation, see example below:

   fun validBookingRequest(bookingRequest: BookingRequest) {
if (bookingRequest.from == bookingRequest.to) {
throw InvalidBookingRequestException("departure and arrival stations are the same")
} else if (bookignRequest.stops > 5) {
throw InvalidBookingRequestException("more than 5 stops is not allowed")
}

}

The Step-down rule

We write code to be read, so writing functions in an order like a narrative text, if we have to put the functions of the above two examples, they should appear in the following order:

   fun bookTrain(bookingRequest: BookingRequest): Booking {
validBookingRequest(bookingRequest)
...

fun validBookingRequest(bookingRequest: BookingRequest) {
...

we can see clearly that the caller function is above the called one

Switch statements

While switch statement can easily impact badly you clean code, The key issue with switch statements is that they often lead to violations of the Single Responsibility Principle (SRP) and can make code harder to extend and maintain.

   fun calculateWashCost(vehicle: Vehicle): Money {
when (vehicle.type) {
CAR -> calculateCarWashCost(vehicle)
BUS -> calculateBusWashCost(vehicle)
MOTOCYCLE -> calculateMotoCycleWashCost(vehicle)
else -> {
throw InvalidVehiculeType(vehicle.type)
}
}
}

There many issues with this function above, first the function is large and each time new vehicle type will be added, it will grow even more. Second it violates the Single Responsibility Principle (SRP) because there is more one reason for it to change, but the worst probem is there will be more functions that will have the same structure:

  • CalculateParkingCost(vehicle: Vehicle): Money
  • CalculateCarbonTax(vehicle: Vehicle): Money

A solution proposed by Robert C.Martin is his book "Clean Code" is to hide the switch statement in an abstract factory, and the factory will use the switch statement to create the appropriate instances of the derivatives of Vehicle. And the various functions such as CalculateParkingCost, CalculateCarbonTax will be dispatched polymorphic through the Vehicle interface.


abstract class Vehicle {
abstract fun calculateWashCost(): Money
abstract fun calculateParkingCost(): Money
abstract fun calculateCarbonTax(): Money
}

abstract interface VehicleFactory {
abstract fun createVehicle(vehicle: Vehicle): Vehicle
}

class VehicleFactoryImpl() {
fun createVehicle(vehicle: Vehicle): Vehicle {
return when (vehicle.type) {
CAR -> Car(vehicle)
BUS -> Bus(vehicle)
MOTOCYCLE -> MotoCycle(vehicle)
else -> {
throw InvalidVehiculeType(vehicle.type)
}
}
}
}

Functions common patterns

Don't hesitate to make your function's name long if necessary in order to ensure a significant name. When it comes to function argument the ideal number is 3, then comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. The challenge with arguments resides in testing you can imagine the difficulty of writing all the test cases to ensure that all the various combinations of arguments work correctly. Have you ever heard about "Flag Argument"? Flag argument is an argument of type boolean where the function do a thing when it's true and another thing if it's false, these arguments violates the Single Responsibility Principle (SRP).

Argument Objects

If a function needs more than two or three arguments, there is probably a way to wrap some of them into an object, see the following example:

  fun deployApplication(applicationId: Int, cpu: Int, memory: Int, storage: Int, tag: String) {
// do something ...
}

We can reduce the number of argument by passing an object representing the infrastructure requirements, see example below

  fun deployApplication(applicationId: Int, infrastructureRequirements: InfrastructureRequirements, tag: String) {
// do something ...
}

Command Query Separation

The Command-Query Separation (CQS) principle states that a function should either perform an action (a command) or return data (a query), but not both. This makes the code more predictable, easier to test, and cleaner.

  // Query function: returns whether the withdrawal can happen (no state modification)
fun canWithdraw(balance: Int, amount: Int): Boolean {
return amount <= balance
}

// Command function: performs withdrawal by returning the new balance (state modification, no return of query data)
fun withdraw(balance: Int, amount: Int): Int {
return if (canWithdraw(balance, amount)) {
balance - amount // Returns the updated balance
} else {
balance // No changes if insufficient funds
}
}

fun main() {
var balance = 100

// Query if withdrawal is possible
if (canWithdraw(balance, 50)) {
// Command: Update the balance by performing withdrawal
balance = withdraw(balance, 50)
println("Withdrawal successful. New balance: $balance")
} else {
println("Insufficient funds")
}
}

Conclusion

Let's admit that functions are fundamental components of our code, so it's crucial to invest time and effort into defining them properly, including their names, arguments, and statements. Writing software is similar to any other form of writing—you begin by drafting your ideas, then refine them until they flow smoothly. Remember, we write code not just for execution, but also to be easily understood by others.


Clean code: Write The Code You Want To Read (Part 1)

· 6 min read
Reda Jaifar
Lead Developer

author photo source

Clean Code!

Why should I care?

We, software engineers almost spent more time reading code than writing new lines, how many times do we complain about someone else's code? Many factors can give us an idea about the quality of code and how much the writer cares about it. If you dislike reading bad code, you already made your first step toward writing good code if you care about your heritage. There are many good reasons to care about writing clean code, adding the artistic layer to your code is an inspiring reason for me to learn and apply the clean code rules and principles.

What clean code brings to me?

Clean code is what makes us professional programmers, someone with high-level ethics who cares about the present and the future of his code, he believes that lines of code can live for long and can be enhanced by others with ease and passion. Like a book author what makes him happy is how readers enjoy turning the pages of his book one after the other without realizing the time elapsed.

Clean code is about philosophy!

Clean code makes us more than a programmer, it helps us develop a good vision of the software we are building, caring about its growth, evolutionary, and enhancement. Clean code makes us a thinker about maintainability, design, and the ability of the software to cope with changes quickly and easily. Code is written to live but also to change and evolve.

We are authors

Yes, we programmers are authors, that said, we have readers, Indeed we are responsible for communicating well with readers. The next time we write a line of code, we'll remember we are authors, writing for readers who will judge our effort.

What clean code covers?

Naming

As a programmer the first step of writing code is choosing names, for variables, functions, classes, packages and source code files. While this seems easy and instinctive, choosing good names takes time but saves more than it takes.

Let's look at the following code snipped:

 d = Date.now();

The name "d" above has nothing to reveal, even tough it is a date object, but we cannot know the intention of usage, either its start date or end date. Note that even naming it startDate doesn't give it any sense, because we need to know as a reader what is the context of the start date. Hers is a suggestion for this example:

taskStartDate = Date.now();

Abbreviation

Abbreviation Is one of the most common mistakes concerning variable naming, as a programmer can you guess what this variable name below means?

msg

Can you know that msg may mean "message" or "most scored goal"? Personally, I don't want to spend time exploring many lines before or after this one to understand the context of this variable in case I need to make a change. The rule is to avoid any disinformation.

Distinction

Another issue with naming is the number-series such as (variable1, variable2), consider the following function:

public static void duplicateString(char a1[], char a2[]){
...
}

is it not more readable if we use "source" and "destination" as the names of the two arguments? I think yes, it is.

Adding noise words is another problem that impacts the cleanness of the code, you may want to specify that a variable is a String, so you name it: "emailContentString", Here the "String" is just redundant as is not part of the name but the type which has nothing to do with the meaning of the variable.

Word Sounding

As a programmer, there is a good chance that while we are writing code, our brain is pronouncing the text we type. when we cut the connection between our brain and the activity of writing, we usually type variable names that could be difficult to pronounce, and the consequences are multiple: other developers won't be able to retain them easily and these names will be demanding to discuss with the business analysts. While English is the most used natural language used to write code, using other languages such as French or Italian, apply the same rules regarding how easily the variables, functions, or classes names are pronounceable.

Are names accessible?

Each time I take over developing a new feature or fixing a bug that requires modifying a source code that not has been writing by me, I start by searching some keywords that I got from the context of the domain system. For Example when I was asked to fix a UI bug in a web application developed using ReactJS then I was trying to find the matching component in the source code, but it was not as easy as expected and I spent 30 minutes before finding the component named with a number prefix: 1CounterComponent. This is why choosing names that are straightforward to find is a very useful rule to follow.

Coding Conventions

Every programming language provides coding conventions regarding variables, functions, classes, and naming source code files. While the naming took a good part of these conventions, they also cover indentation, comments, declaration order, etc ... I don't hesitate to refer to these conventions. But during my modest experience, I came across some coding conventions from specific programming languages applied to another one. This is strongly discouraged or prohibited by the teams themselves.

Technical Vs Business Names

We write code to build software that will be solving a problem, For example: coding an application that computes taxes. Trying to be a good programmer implies differentiating technical things from business-related ones. whenever you code a technical concept don't try to use mainly a domain name, For example: declaring a variable that holds an instance of the HTTP client could have the following name: httpClient, but if we try to include the business-related usage we can name it: taxesRulesHttpClient as you can see in this case the domain doesn't bring any help instead is just making a technical thing harder.

A last note

Writing clean code requires a piece of cultural knowledge and good descriptive, communication, and writing skills, we can develop these skills by learning from communication experts either by reading books or taking courses on how to write, synthesis, and order ideas. Also evolving on the natural language we use to code. For example, if we write code in English, it will be helpful to learn more words, synonyms, sentences, etc...

So far I wanted to pay your attention to the importance of clean code, and how can impact the software's quality and maintenance, We covered mainly the naming concept in this part. Other articles will follow to cover other aspects concerned by clean code.


Introduction to software functional and behaviour testing

· 7 min read
Reda Jaifar
Lead Developer

author photo source

Functional Testing

What is a functional test ?

Functional tests are one of these software testing approaches or test types such as (unit tests, integration tests, load tests, penetration tests, ...) all with one mission to test that the software is compliant whether with business specification, technical requirements or other quality and usability metrics. But functional tests focus on ensuring that the software functions behave as expected by the business specifications, these tests don't interact with source code such as unit tests, but mainly with the software features. A functional test usually puts the system, the application or the software we want to test in an initial state where we provide the necessary elements to make the test executable such as storing a list of cars in the database, then we test the feature find a car for the period of (2nd march to 7th march), then we validate that the output matches the expected result.

Why do we need to write functional tests?

We need to write functional test to validate the features from a user perspective, hence we validate the following:

  • Feature is working as expected by the specifications
  • Usability: checks whether the feature is easily usable, for example, a button is freely reachable on the page.
  • Errors: when a subsystem is not responding, do we display an error message to the user to help him understand what's going on.

Functional testing style

A common form that functional testing take is the Given-When-Then, this approach coming from the BDD (Behaviour Driven Development) defines the structure of many testing frameworks such as Cucumber that we will cover in our example later in this article. The prime idea is to break down a scenario (test) into three sections:

  • Given: the given part defines the pre-conditions before challenging the system by executing or running a feature.
  • When: is do we want to do with the system, for example ( when I book a car)
  • Then: describes the expected result or output after the application or the software behaves in to respond to your action.

To simplify the idea, let's write an example for a rental car website using the Cucumber Tool (Framework):

Feature: User book a car Scenario: User requests to book a car from 1st March to 7th March 2022 Given I select a car from the available cars for the period (1st March to 7th March 2022) And I select GPS as an additional Option And I select Full Insurance When I book the car Then I should receive a confirmation

BDD: Behavior Driven Development

BDD combines the best practices of Test Driven Development TDD, Domain-driven Development (DDD), and Object Oriented Programming (OOPs) For an agile team, scoping a feature is a very important task, as the stakeholders are talking about the business requirements, the development team is more interested in the technical challenges, Here comes the BDD to provide a common language that allows efficient communication and feedback and then a perfect specification, development vision, and feature delivery.

BDD closes the gap between the business and the technical people by:

  • Encouraging collaboration across roles to build a shared understanding of the problem to be solved.
  • Working in a rapid and small iteration to promote the feedback and optimize the value delivery.
  • Producing documentation that is automatically checked against the software behavior.

There is a good chance that you're agile at your organization so you already plan your work in small increments of value like User Stories. In this case, BDD will help you to deliver your promises of agile on time. BDD does not replaces your processes but enhances them.

BDD and Functional Testing

Let's focus on the word Behaviour so functional testing of behavior testing is these tests your write to check your system or the software you're building how behaves. Functional testing can also be called behavior testing.

Behaviour Testing in action

To illustrate all these abstract notions explained briefly in this article, let's write a small application and its behavior tests using Kotlin programming language and Cucumber

  • Kotlin is a JVM programming language, like Java, Scala, or Groovy
  • Cucumber is a testing tool that supports Behavior Driven Development
  • Gherkin is a business readable language that helps you to describe business behavior without going into details of implementation

We will need the following to build this example:

  1. Java SE (Java 9 and higher are not yet supported by Cucumber)
  2. Maven - version 3.3.1 or higher
  3. IntelliJ IDEA (which will be used in this tutorial)
  1. Clone the project from github
git clone https://github.com/reda-jaifar/hands-on-kotlin.git
cd sportair
  1. Open the project in IntelliJ IDEA:

    • File -> Open… -> (Select the pom.xml)
    • Select Open as Project
  2. Verify Cucumber installation

mvn test

Now our environment is ready, let's write some scenarios for the following application:

SportAir is an application that indicates whether we can exercise outside or not based on the weather.

In Cucumber, an example is called a scenario. Scenarios are defined in .feature files, which are stored in the directory (or a subdirectory).

Create an empty file called src/test/resources/sportair/can_we_exercice_outtside.feature with the following content:

Feature: Can we exercise outside? Everybody wants to know if we can exercise in the air

Scenario: The weather is not convenient for exercising outside Given The temperature is 42 celsius When I ask whether I can exercise outside Then I should be told "Nope"

if you're using Intellij Idea Cucumber Plugin, you should see the keyword colored, below the meaning of each:

  • Feature: is a keyword that should be followed by the feature name, a good practice is to use the name of the file. The line that follows is a description that will be ignored by Cucumber execution parser.
    NB: We use a feature by file
  • Scenario: defines the name of a scenario, we can have as many scenarios as expected by a feature.
  • Given, When, Then: are the steps of the scenario. Refers to the definition above.
mvn test
The output should be something like the following:
Given The temperature is 42 # StepDefs.The temperature is(int)
When I ask whether I can exercise outside # StepDefs.I ask whether I can exercise outside()
Then I should be told nope # StepDefs.I should be told(String)

Scenario: The weather is convenient for exercising outside # sportair/can_we_exercice_outside.feature:9
Given The temperature is 24 # StepDefs.The temperature is(int)
When I ask whether I can exercise outside # StepDefs.I ask whether I can exercise outside()
Then I should be told of course # StepDefs.I should be told(String)

2 Scenarios (2 passed)
6 Steps (6 passed)
0m0.181s

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.403 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

Behavior Driven Testing Benefits

  • Helps document specification by the usage of non-technical language
  • Focuses on how the system should behave from both user and developer perspective
  • Gives high visibility of the system design
  • Helps to make the software or the system meet the user need

The figure below illustrates the process of BDD and how it can help to write down behavior tests.

BDD Process

This figure defines a step flow to help define and write down behavior tests