All Blogs

Bilgin Ibryam

|

September 27, 2024

Mastering the Dapr Inner Development Loop - Part 1: Code & Unit Test

Building and testing distributed applications is hard. Open source Dapr helps by providing a set of APIs that codify distributed systems best practice for all languages. In this article, we will explore the common stages and tools used in Dapr development during the inner development loop using a sample distributed application.

Dapr supports various languages and runtimes, such as Spring Boot in Java, Flask in Python, NodeJS in JavaScript, Go, Rust, and others. Each language and framework has its own development techniques and optimized flow, and Dapr fits with any of them. Despite the different options, a recent Dapr user survey indicates that three-quarters of Dapr users deploy it with Kubernetes in production environments. Therefore, your application needs to be cloud-native, and Dapr should be configured with best practices for Kubernetes deployment before being pushed to source control and deployed to higher environments.

In this series of 3 articles, we will explore the common stages and tools used in Dapr development during the inner development loop using a sample distributed application.

We'll demonstrate how to quickly deploy these changes on a local Kubernetes cluster using open source Skaffold and check for Dapr best practices in development using Diagrid Conductor. This approach aligns with the environment parity principles of 12-factor apps and shift-left principles where issues are identified in early environments and ensures that applications are as similar to the  production environment as possible from the very beginning.

Implementing Applications with Dapr APIs

When writing code, developers repeatedly build and deploy their work-in-progress to a development environment to check functionality, identify errors, and iterate. This process is known as the inner loop. When deploying locally for Kubernetes, this inner loop is a collection of loops, each executed fast:

  1. For example, in the innermost loop, a Java developer might create a class and run unit tests to validate it.
  2. Then, in the second step, once the application structure is ready, the developer can build and package it as a web app or an uber jar and run the application locally to ensure it starts up correctly and its endpoints accept connections.
  3. If the application will be orchestrated on Kubernetes, there is one more important loop: after a few interactions, the developer may deploy the application to a locally running Kubernetes cluster to validate all necessary containerization and orchestration configurations.

In this first article we'll cover the first of these: creating a class and running unit tests to validate.

Inner development loop phases for Dapr

These are all phases of the inner development loop, executed locally and in rapid succession. These are roughly the stages we will follow in this article, exploring common tools and areas to pay attention to for the best results. When developing locally, the inner loop focuses on writing application code and validating it as quickly as possible. Let’s look into the smallest inner loop first.

Writing Code using Dapr SDKs

The core of a distributed application lies in your application and its business logic. During this phase, you use your preferred language and framework with your IDE to implement your application leveraging Dapr SDKs. Dapr is agnostic to development techniques and architectures, including Domain-Driven Design (DDD), Hexagonal architecture, Test-Driven Development (TDD), Behavior-Driven Development (BDD) and more. It integrates seamlessly, without imposing restrictions on the granularity of your application or constraining how you test and iterate writing your code. The nice thing about Dapr is that it stays at the edge of your application and acts as the sole intermediary, facilitating connections with other applications and external systems. From within your application code, you use Dapr SDKs as the single library to interact with infrastructure resources  like message brokers, key/value stores, configuration mechanisms, secret stores, and more. This approach abstracts the complexity of integrating with diverse systems, allowing you to focus on business logic. For instance, consider a scenario where your Java application needs to publish events to a message broker. With Dapr, you can use the Dapr Java SDK to send messages to the broker, without worrying about the underlying implementation details or message broker semantics.

// Publish messages
@PostMapping(path = "/pubsub/orders", consumes = MediaType.ALL_VALUE)
public Mono<ResponseEntity> publish(@RequestBody(required = true) Order order) {
    return Mono.fromSupplier(() -> {
    
        // Publish an event/message using Dapr PubSub
        try {
            client.publishEvent(PUBSUB_NAME, "orders", order).block();
            logger.info("-> Order published: " + order.getOrderId());
            return ResponseEntity.ok("SUCCESS");
        } catch (Exception e) {
            logger.error("Error occurred while publishing order: " + order.getOrderId());
            throw new RuntimeException(e);
        }
    });
}

A sample Dapr Java SDK code block to publish an event on a message broker

This abstraction not only simplifies development and enhances portability and consistency across environments, but also simplifies the mocking and unit testing of your application.

Unit Testing Dapr App Code

In this stage, the primary task is to validate the code through unit and component tests, ensuring the logic in small units such as classes, functions, and methods is functioning correctly. The goal is to have fast, reliable tests that do not depend on external systems. Mocking the Dapr client is the best approach here, allowing you to validate business logic without interacting with real databases, message brokers, or other external systems.

@Test
public void testPublishOrderSuccess() {
    Order order = new Order();
    order.setOrderId(1);

    when(daprClient.publishEvent(anyString(), anyString(), eq(order))).thenReturn(Mono.empty());

    Mono<ResponseEntity> responseMono = controller.publish(order);

    StepVerifier.create(responseMono)
            .expectNext(ResponseEntity.ok("SUCCESS"))
            .verifyComplete();
}

A sample Java unit test mocking Dapr Client

Since the Dapr SDK is the interface to your backing infrastructure, mocking the Dapr SDK allows you to isolate all external dependencies uniformly, rather than mocking each technology individually, such as Kafka or Redis clients. This is a common development and iteration technique in languages like Java and .NET, although other languages may follow different iteration patterns. Regardless of these differences, the aim is to rapidly write code focused on business logic and consistently prove it works as intended. This helps maintain high levels of developer productivity and rapid iteration during the software development lifecycle.

Read Part 2 of this series where we will dive into running and validating applications locally.