Declarative Style in Reactive Programming – Take 2

Reactive programming is tricky. I have written about it before here. It is not just learning a library and choose methods to use. It require changing the logic flow to be declarative and that need some time to practice. I have been using reactive programming for almost a year and still find myself creating bugs due to not enough knowledge in the API, pattern and the style.

One recent bug I created was while I am writing a testing stub for UserService interface. Here is the simplified code to demonstrate the issue.

interface UserService {
    fun newUser(
           userId: UUID, 
           userName: String, email:String
    ) : Mono<Void>

    fun getUser(userId: UUID) : Mono<User>
}

class IncorrectUserServiceStub : UserService{
    private val users = mutableMapOf<UUID, User>()

    override fun newUser(
         userId: UUID, 
         userName: String, 
         email: String): Mono<Void> {

       //println("Construct mono for creating new user")
       return Mono.fromRunnable<Void> {
           //println("Creating new user")
           users[userId] = User(userId, userName, email)
       }
    }

    override fun getUser(userId: UUID): Mono<User> {
        //println("Getting a user")
        return users[userId]?.toMono() ?: Mono.empty()
    }
}

The stub implementation is backed by simple hash map. The logic is plain simple. Just put things in and getting things out of hash map. Now I try to test the stub itself since I know reactive programming could be tricky. Something looks so simple might go wrong easily. Here is the test code.

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import reactor.test.StepVerifier
import java.util.*

class UserServiceStubTest {
    private val userService = IncorrectUserServiceStub()
    //private val userService = CorrectUserServiceStub()

    @Test
    fun `create new user and query the user back by userId`(){
        val userId = UUID.randomUUID()
        val resultMono = userService
            .newUser(userId, "userA", "userA@gmail.com")
            .then(userService.getUser(userId))


        StepVerifier
            .create( resultMono )
            //verify the result of userService.getUser(userId)
            .assertNext { user ->
                assertEquals("userA", user.name)
                assertEquals("userA@gmail.com", user.email)
            }
            .verifyComplete()
    }
}

The test will fail with error that looks like the created user is not in the system. How could a logic with just simple hash map goes wrong.?

expectation "assertNext" failed (expected: onNext(); actual: onComplete())


Declarative Style

The bug will be easier to understand when we uncomment the println() statement. We will see that the line to get a user is execute before the line that actually create the user.

Construct mono for creating new user
Getting a user
Creating new user

In declarative style, the method getUser(userId: UUID): Mono<User> should just define a logic wrapped in reactive construct and return. It is like saying here is the logic to be run but when it will be run is up to you. It might get executed 1 millisecond later or 5 minutes later. The method is to chain together graph of logic that will be run but not actually running the logic itself. It is just declaring things.

The correct implementation is shown below.

override fun getUser(userId: UUID): Mono<User> {
  println("Construct mono for getting a user")
  return Mono.defer{
      println("Getting a user")
      users[userId]?.toMono() ?: Mono.empty()
  }
}

Now the test pass and println() order looks about right.

Construct mono for creating new user
Construct mono for getting a user
Creating new user
Getting a user

The reactive programming is tricky. There were times that I spent a couple hours to figure out a bug in simple execution and it turned out that it was just because I incorrectly arranged the chain of logic in reactive style.

You may find the example code in git here.