Fl4m3Ph03n1x

Fl4m3Ph03n1x

Background

I have a behaviour that has some functions inside it, so that modules that implement it can use them:

defmodule MyBehaviour do

  #some callback for other modules to implement
  @callback match(data :: any) :: boolean

  defmacro __using__(_opts) do

    quote do
      @behaviour MyBehaviour
      
      def build_query(some_str) do
        String.slice(some_str, 1..-1) #random op on string
      end

    end
  end
end

Problem

However, I need to test if build_query is working properly. I can’t invoke it and test it normally via MyBehaviour.build_query/1 because technically it doesn’t belong to the behaviour, it is instead inside the macro.

Question

How do I access MyBehaviour.build_query/1 in order to test it?

Showing Posts 1 to 10

LostKobrakai

LostKobrakai

If the function does not need data from within the macro just move the function in your behaviour and let the macro import it. If if does need data from the macro, then you need to call the macro from some module to test it on.

NobbZ

NobbZ

defmodule FooTest do
  defmodule FakeImplementor do
    use MyBehaviour
  end

  test "build the query" do
    # whatever is necessary to check `build_query/1` by calling it through `FakeImplementor`
  end
end

This is one way.

The other way (which I’d prefer) were to not inject it, but import it and also make it unimportable through options.

devonestes

devonestes

If the function isn’t some default implementation for the required callbacks for your behaviour, I would contend that it probably shouldn’t be a part of the __using__ macro at all. No need to even import it in the __using__ macro, as that will duplicate the implementation of that function across all modules that use that behaviour.

I would (personally) do this:

defmodule MyBehaviour do

  #some callback for other modules to implement
  @callback match(data :: any) :: boolean
      
  def build_query(some_str) do
    String.slice(some_str, 1..-1) #random op on string
  end
end

defmodule MyImplementation do
  @behaviour MyBehaviour

  def match(data) do
    data
    |> MyBehaviour.build_query()
    |> is_valid?()
  end
end

Then you test that function directly as any other function, and you don’t have duplicate implementations of the same functionality (which is basically what’s happening when you import anything). If your implementations of that behaviour need variations on that function, you can replace MyBehaviour.build_query/1 with a locally defined variation on that function.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

@NobbZ
This is an approach I also took. However I do have the feeling that instead of manually making a FakeImplementor ( which is in reality a mock ), I should be using real mocks, in specific, I should be using the library mox with contracts and all. However I am not yet up to that level and so for the time being I need an intermediate solution.

@devonestes
This was actually my original approach. Since every module that implements this behaviour will likely use this function, I saw it as a good idea to simply put it inside the macro.


My original idea here is to follow something among the lines of use GenServer. You can access and use a ton of their functions, but you don’t do it via GenServer.some_function, you just call some_function and you are done with it.

My idea would be to do the same with a simple module, but I don’t quite know how to test it. How did the guys from Elixir team test their implementations? By using FakeImplementors? Mox? There has to be a way.

LostKobrakai

LostKobrakai

Elixir also uses a module created just for testing: elixir/lib/elixir/test/elixir/agent_test.exs at 7f4756d401a4e59f531484f96990f710d41be540 · elixir-lang/elixir · GitHub

I mean that’s in the end exactly what you want to make sure works: Using it in another module. It’s easy to do so why not test it that way?

NobbZ

NobbZ

What ton is this? I’m not aware of a single one…


edit

Took a look at the source code, GenServer does not put anything into your module, except for the child_spec and the default callback implementations:

devonestes

devonestes

They don’t test behaviour definitions in Elixir, they only test the actual implementations. For example, they have a Calendar behaviour, but they only test the different calendars themselves, not the behaviour on its own using a fake implemenation or something. IMHO this is the right thing to do. Test the implementations, not the behaviour.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

How do you know if handle_call is correctly implemented)? Child_spec? ( how would you test them? )
You automatically include them in your code when you use the GenServer. These functions are tested, right?

This is what I am trying to understand. Is it only possible to test functions inside macros by using a FakeImplementor? If so, Wouldn’t it be better to use moxx ?


PS: I do agree with the overall notion that one should extract the function out of the macro and into the behaviour, but I am trying to understand how one would test functions inside macros.

NobbZ

NobbZ

The default implementations are not tested at all, they just logg if they were called to avoid a spilling message queue, as soon as you see such a message in production, you should implement a proper handler that deals with those messages. If you have at least one handle_X, the default one isn’t used any more and your GenServer would crash on an unknown message for that handler.

child_spec is probably not tested as well, it just returns a constant map and doesn’t change.

If at all you’d test the __using__ macro if it creates the child_spec function correctly, as you need to specify some things that child_spec shall return as an argument to the use.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

Alright. So what I get from this discussion is the following:

  1. One should avoid placing functions inside macros as much as possible
  2. Such functions can only be tested via FakeImplementators

My last question, is putting functions inside the use macro considered an anti-pattern in this community?
What are the cases one should and should not do it?

I am merely asking for online resources to read, since I failed to encounter any.

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews