Hi all, what do you think of having function dependency in Module / Protocol / Dynamic Function & it’s relation to testing & mocking. Which one you prefer and why?
For example, let’s say we have a function to validate user, and to validate this user we need to call external API.
For help with clarification, let me try to make example of such function dependency in Module / Protocol / Dynamic Function
Module: usually tested using Mox
def validate_user(user_data) do
has_banking_account? = third_party_client().check_user_exist?(user_data["social_security_number"])
...
end
defp third_party_client do
Application.get_env(:my_app, :third_party_client)
end
Protocol
def validate_user(user_data, opts \\ []) do
third_party_client = opts[:third_party_client] || RealThirdPartyClient.new()
has_banking_account? = ThirdPartyClient.Behavior.check_user_exist?(third_party_client, user_data["social_security_number"])
...
end
Dynamic Function
def validate_user(user_data, opts \\ []) do
third_party_check_user_exist? = opts[:third_party_check_user_exist?] || fn ssn -> ThirdPartyClient.check_user_exist?(ssn) end
has_banking_account? = third_party_check_user_exist?.(user_data["social_security_number"])
...
end
You need to tell the third-party client to change the base URL. That’s not always possible, but if it’s the case - you basically need to solve the same problem, right?
I’d pass it via a param if I only need to mock/replace it for the test that exercises validate_user:
defmodule MyApp.ValidatorTest do
# ...
test "validation" do
assert ... = MyApp.Validator.validate_user(user_data, mock)
end
end
But that doesn’t work great if you need to mock the third-party client in an end-to-end/integration test. This would force you to push the config from some top-level function down to the module that uses the client. For such cases - which I think are the most common - I prefer pulling this from the config via Application.get_env or similar.
For me, what’s really make me reconsider this problem, is that i was working on a really large module (probably should be refactored to smaller module), and mocking with module that i usually does become really hard to be traced, which function that depend on which module.
So i was working to implement feature in TDD style development, and i was playing around with lifting up dependency as Dynamic Function, to my surprise it work quite well and general development workflow feel pretty nice. it’s integration does take more effort though.