Elixir works correctly

I'm new to elixir and functional programming in general and I struggle to do unit test functions that are composed of other functions. General question: when I have a function f that uses other functions g , h ... internally, what approach should I take to check everything?

From the OOP world, the first approach that comes to mind involves function injection f . I could unit test g , h ... and insert all these arguments into f . Then, unit tests for f , just make sure that it calls the injected functions as expected. It's kind of like overfitting, and as a general cumbersome approach, contrary to functional thinking, for which the composition of functions should be cheap and you shouldn't be concerned about yourself passing all these arguments around your entire code base.

I can also unit test g , h ... as well as f , treating each of them as black boxes, which feels like the right thing to do, but then the complexity of the f tests increases dramatically. Having simple tests, scale is one of the main goals of unit testing.

To make the argument more specific, I'll give an example of a function that contains other functions inside and that I don't know how to unit test correctly. This is in particular the code for the plugin that handles the creation of a RESTful resource. Note that some of the "dependencies" are pure functions (for example validate_account_admin), but others are not (Providers.create):

  def call(conn, _opts) do
    account_uuid = conn.assigns.current_user.account["uuid"]

    with {:ok, conn}      <- Http.Authorization.validate_account_admin(conn),
         {:ok, form_data} <- Http.coerce_form_data(conn, FormData),
         {:ok, provider}  <- Providers.create(FormData.to_provider(form_data), account_uuid: account_uuid) do
      Http.respond_create(conn, Http.provider_path(provider))
    else
      {:error, reason, messages} -> Http.handle_error(conn, reason, messages)
    end
  end

      

Thank!

+3


source to share


1 answer


Perhaps this will be a rather subjective answer, because there can be no ideal and final answer for such a question.

Your assumption is wrong for me in terms of using public functions inside other public functions. You shouldn't do this at all in the areas of business logic, because they should be separated from each other and the only place where you can do this, and in fact, you should be in controllers, but you are testing controllers with integration tests. rather than unit tests, so all you need in such tests are correct and reliable answers.

I like Erlang's explicit approach to declaring which features should be publicly available using a proposal export

. In Elixir, you must also follow this approach, and anything that needs to be hidden in a module must be declared with defp

and defmacrop

accordingly for private functions and private macros.

Your unit tests should follow the black box rule - you care about output based on input. All this. The test is dumb and does not know at all what the function under the test looks like and what it contains.

In your example, you are using some of the functionality in the Plug function call

, and I'm sure this plug does more than it needs to - remember single responsible principle

. This makes this feature nearly impossible to test without mockery ... I would rewrite this plug into 3 or 4 four separate connectors because the proposal with

is redundant - the plugs check the outcom of the previous plug to continue - it's case

inside case

, which does with

.

Considering you have new plugs, you can use some additional functions inside the plug besides call

and init

that do the real work, defined as private functions, and this action can help you organize your code and avoid creating related modules in terms of use and responsibility.

Then unit tests will be much easier because you will be testing isolated plugs.



Assuming you have this widget, like this:

plug MyPlug

      

you rewrite:

plug :validate_is_admin
plug :coerce_form_data
plug :create_from_form_data

      

This may be oversimplified, but I hope you understand what I mean here.

TL; DR: Split functions into smaller ones and test them in isolation. Hide internal computations in private functions and check only public API.

+3


source







All Articles