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);
}
The reason you aren't seeing the point of FCIS is because you are hiding a lot of the logic this function needs. If we break down what this method is actually doing, it can be made more clear so let's do that:
function createOrder(userInfo, addressInfo, orderInfo) {
First you are calling findUser
in the userApi
. This function does the following:
userInfo.email
valueThen you take the boolean from above and call either createUserAdress
or updateUserAdress
again, this means you are:
userInfo
value to create another DB command.Let's pause there and see what we would have using FCIS...
email
and returns a DBCommand
.userInfo
and returns a DBCommand
.DBCommand
, sends it to the DB and optionally returns a value.So if developed with FCIS in mind, you would have:
const findUser = createFindUser(userInfo.email);
const userExists = this.api.perform(findUser);
const upsertUser = createUpsert(userExists, userInfo);
this.api.perform(upsertUser);
And we would write tests for both create
functions, neither of which would need a mock to test. (BTW, there is no assumption here that userExists
is a bool; the only assumption is that it's something the createUpsert
command can use.)
The same process would hold for the user's address, and the order creation.
In the final code then, we would have five pure functions, each of which is fully testable without mocks, and a single impure function on a single api
object. Like this:
function createOrder(userInfo, addressInfo, orderInfo) {
const findUser = createFindUser(userInfo.email);
const userExists = this.api.perform(findUser);
const upsertUser = createUpsert(userExists, userInfo);
this.api.perform(upsertUser);
// I went with a more compact expression for the below,
// but it's the same structure.
const addressExists = this.api.perform(createFindAddress(addressInfo));
this.api.perform(createUpsertAddress(userInfo.email, addressExists, addressInfo));
this.api.perform(createOrder(orderInfo));
}
So instead of a UserAPI class with three methods all of which need to be tested using mocks and an AddressAPI class with three methods (again, all of which need to be tested using mocks), and an OrderAPI class with a method (again needing a mock to test), and lastly our principle function that needs three mocks to test... you only will be testing five functions, none of which need mocks.
Would you bother testing the principle function in this case? Likely not; after all it doesn't have any logic in it. It's just straight line code. But if you did choose to test it, you would only need a single mock instead of three.
Functional Core, Imperative Shell is a clear winner for both the simplicity of the needed tests and a reduction in the number of tests needed.