Search code examples
unit-testingfunctional-programming

Approach to functional core, imperative shell


I saw several people talking about functional core and imperative shell and how it relates to unit tests, avoiding mocks, etc... However, I can't see refactoring situations in cases with little domain logic and more side effects. What would be a good approach to refactor and unit test a scenario like this:

function createOrder(userInfo, addressInfo, orderInfo) {
    const userExists = this.userApi.findUser(userInfo.email);
    if(!userExists) this.userApi.createUser(userInfo);
    else this.userApi.updateUser(userInfo);

    const adressExists = this.userAdressApi.findAdress(addressInfo);
    if(!adressExists) this.userAdressApi.createUserAdress(userInfo.email, addressInfo);
    else this.userAdressApi.updateUserAdress(userInfo.email, userAdressInfo);

    this.orderApi.create(orderInfo);
}

Solution

  • The point of functional core, imperative shell is to implement all the heavy decision logic typically associated with business logic as pure functions, and then having an imperative shell that handles all the impure stuff.

    The code in the OP has almost no logic, and what little there is is exclusively related to I/O.

    There's no Domain Model in the OP, but rather a Transaction Script. Furthermore, the three blocks of code are unrelated to a degree that they could be executed in parallel. Thus, to quote Gertrude Stein, "there is no there there".

    If you really want to, you could turn that little check-the-record-and-upsert-it-accordingly operation into a reusable action if you'd like, but it almost doesn't seem worthwhile.

    I don't know which language the OP is in, but in Haskell it might be something like this:

    readAndUpsert :: Monad m => m Bool -> m b -> m b -> m b
    readAndUpsert r u c = do
      b <- r
      if b then u else c
    

    It's almost too simple to unit test, but if you wanted to test it, you could test it using a deterministic monad, most easily with the Identity monad.

    Here's an example of a composition that instead of reading from a database takes input from the console and prints to the console instead of writing to the database:

    ghci> readAndUpsert (("yes" ==) <$> getLine) (putStrLn "update") (putStrLn "create")
    yes
    update
    ghci> readAndUpsert (("yes" ==) <$> getLine) (putStrLn "update") (putStrLn "create")
    no
    create
    

    This composition checks if the input typed at the console is yes. If so, it performs an update; otherwise, it performs a create.

    A related example might be a little API for idempotent creates. Usually you don't need to go to such lengths, but it's nice to have the option.

    A common learning is that once you start untangling the actual business decision logic from all the I/O, there's often not a lot left.