System Dependency & Testing: Pro & Cons of Module vs Protocol vs Dynamic Function

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

There is yet another option.

Do not mock your code, but provide fake service via something like bypass library.

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?

Yeah, in the end it’s a question of where to push the need for something to be dynamic instead of hardcoded.

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.

1 Like

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.