mudasobwa

mudasobwa

Creator of Cure

Bex - set of mix tasks to help dealing with behaviours and mocks

The goal of this library is to make planting better testing into a source code
as smoothly and fluently as possible. It’s barely needed for experienced developers,
but I always find myself struggling to recall all the places in the source code
I have to amend to convert a bare call into a behaviour-baked implementation.

The main task Mix.Tasks.Bex.Generate would do the following things:

  • generate behaviour code for the function(s) given as an argument
  • generate the default implementation for it, wrapping the call to the original code and [optionally] adding :telemetry events in the recommended telemetry.span/3 flavored manner
  • find all the occurrences of the behaviourized call(s) in the source code and patch them in-place (unless --no-patch flag is given)
  • generate test(s) for the aforementioned functions, with proper Mox allowances (unless --no-test flag is given)
  • prompt to amend config/config.exs file to use correct implementations in different environments

More at bex v0.2.0 — Documentation

Most Liked

astery

astery

I now it could be lengthy, but it would be nice to see an example of changes that will be introduced.

mudasobwa

mudasobwa

Creator of Cure
git clone git@github.com:am-kantox/bex
cd bex
mix deps.get
mix compile

This is the only file in the sources having Process.send_after/4 calls.

cat test/support/source.ex
defmodule Bex.Test.Process do
  @moduledoc false

  alias Elixir.Process, as: P1
  alias Process, as: P2
  alias Task.Supervisor

  def schedule_without_as do
    Supervisor.start_link([])
  end

  def schedule(interval) do
    Process.send_after(self(), :schedule, interval, [])
  end

  def schedule_with_fq_alias do
    P1.send_after(self(), :schedule, 1_000, [])
  end

  def schedule_with_alias do
    P2.send_after(self(), :schedule, 1_000, [])
  end
end

Task invocation with all the defaults.

mix bex.generate --function Process.send_after/4

* creating lib/bex/process.ex
✓ Process has been created in lib/bex/process.ex
✓ Suggested change: test/support/source.ex:15 in Bex.Test.Process.schedule(interval)
- Process.send_after(self(), :schedule, interval, [])
+ Bex.Behaviours.Process.send_after(self(), :schedule, interval, [], __ENV__)
✓ Suggested change: test/support/source.ex:19 in Bex.Test.Process.schedule_with_fq_alias()
- P1.send_after(self(), :schedule, 1_000, [])
+ Bex.Behaviours.Process.send_after(self(), :schedule, 1_000, [], __ENV__)
✓ Suggested change: test/support/source.ex:23 in Bex.Test.Process.schedule_with_alias()
- P2.send_after(self(), :schedule, 1_000, [])
+ Bex.Behaviours.Process.send_after(self(), :schedule, 1_000, [], __ENV__)
Apply changes to ‹test/support/source.ex›? [Yn] 
✓ File test/support/source.ex amended successfully
* creating test/bex/process_test.exs
✓ Bex.Behaviours.Process.Mox.Test has been created in test/bex/process_test.exs
Patch ‹config/config.exs›? [Yn] 
✓ config/config.exs has been altered
git diff
diff --git a/test/support/source.ex b/test/support/source.ex
index 620aab9..c3d0829 100644
--- a/test/support/source.ex
+++ b/test/support/source.ex
@@ -12,14 +12,14 @@ defmodule Bex.Test.Process do
   end
 
   def schedule(interval) do
-    Process.send_after(self(), :schedule, interval, [])
+    Bex.Behaviours.Process.send_after(self(), :schedule, interval, [], __ENV__)
   end
 
   def schedule_with_fq_alias do
-    P1.send_after(self(), :schedule, 1_000, [])
+    Bex.Behaviours.Process.send_after(self(), :schedule, 1_000, [], __ENV__)
   end
 
   def schedule_with_alias do
-    P2.send_after(self(), :schedule, 1_000, [])
+    Bex.Behaviours.Process.send_after(self(), :schedule, 1_000, [], __ENV__)
   end
-end
+end

The above is the only diff. Generated:

# lib/bex/process.ex
defmodule Bex.Behaviours.Process do
  @moduledoc false

  _ = """
  Behaviour wrapping Process
  """

  @doc false
  @callback send_after(arg_1 :: any(), arg_2 :: any(), arg_3 :: any(), arg_4 :: any()) :: any()

  # default implementation
  @actual_impls Application.compile_env(:bex, :impls, %{})
  @actual_impl Map.get(@actual_impls, Process, Bex.Behaviours.Impls.Process)

  @doc false
  defdelegate send_after(arg_1, arg_2, arg_3, arg_4), to: @actual_impl

  @doc false
  def send_after(arg_1, arg_2, arg_3, arg_4, %Macro.Env{} = env) do
    event_prefix = Bex.telemetry_event_base(env)

    :telemetry.execute(
      event_prefix ++ [:start],
      Bex.telemetry_measurements_base(%{}),
      %{
        arg_1: arg_1,
        arg_2: arg_2,
        arg_3: arg_3,
        arg_4: arg_4
      }
    )

    result =
      send_after(arg_1, arg_2, arg_3, arg_4)

    :telemetry.execute(
      event_prefix ++ [:stop],
      Bex.telemetry_measurements_base(%{}),
      %{result: result}
    )

    result
  end
end

defmodule Bex.Behaviours.Impls.Process do
  @moduledoc false

  _ = """
  Default implementation for the behaviour wrapping Process
  """

  @behaviour Bex.Behaviours.Process

  @impl Bex.Behaviours.Process
  def send_after(arg_1, arg_2, arg_3, arg_4) do
    Process.send_after(arg_1, arg_2, arg_3, arg_4)
  end
end

case {Code.ensure_compiled(Mox), Mix.env()} do
  {{:module, Mox}, :test} ->
    Bex.Behaviours.Process
    |> Module.concat(Mox)
    |> Mox.defmock(for: Bex.Behaviours.Process)

  {_, :test} ->
    IO.warn("Please add `mox` to deps in `test` env")

  _ ->
    :ok
end
# test/bex/process_test.ex
defmodule Bex.Behaviours.Process.Mox.Test do
  use ExUnit.Case, async: true

  import Mox

  test "Bex.Test.Process.schedule/1" do
    test_process = self()

    Bex.Behaviours.Process.Mox
    |> expect(:send_after, 1, fn _arg_1, _arg_2, _arg_3, _arg_4 ->
      send(test_process, {:schedule, 4})
    end)

    Bex.Test.Process.schedule(nil)
    assert_receive({:schedule, 4})
  end

  test "Bex.Test.Process.schedule_with_fq_alias/0" do
    test_process = self()

    Bex.Behaviours.Process.Mox
    |> expect(:send_after, 1, fn _arg_1, _arg_2, _arg_3, _arg_4 ->
      send(test_process, {:schedule_with_fq_alias, 4})
    end)

    Bex.Test.Process.schedule_with_fq_alias()
    assert_receive({:schedule_with_fq_alias, 4})
  end

  test "Bex.Test.Process.schedule_with_alias/0" do
    test_process = self()

    Bex.Behaviours.Process.Mox
    |> expect(:send_after, 1, fn _arg_1, _arg_2, _arg_3, _arg_4 ->
      send(test_process, {:schedule_with_alias, 4})
    end)

    Bex.Test.Process.schedule_with_alias()
    assert_receive({:schedule_with_alias, 4})
  end
end

Patched/generated:

# config/config.exs
import Config

config :bex,
       :impls,
       (if Mix.env() == :test do
          %{Process => Bex.Behaviours.Process.Mox}
        else
          %{Process => Bex.Behaviours.Impls.Process}
        end)
mudasobwa

mudasobwa

Creator of Cure
defmodule Bex.Behaviours.Process do
  […]
  @callback send_after(dest :: any(), msg :: any(), time :: any(), opts :: any()) :: any()

  […]
  defdelegate send_after(dest, msg, time, opts), to: @actual_impl

  def send_after(dest, msg, time, opts, %Macro.Env{} = env) do
    event_prefix = Bex.telemetry_event_base(env)

    :telemetry.execute(
      event_prefix ++ [:start],
      Bex.telemetry_measurements_base(%{}),
      %{
        dest: dest,
        msg: msg,
        time: time,
        opts: opts
      }
    )

    result = send_after(dest, msg, time, opts)

    :telemetry.execute(
      event_prefix ++ [:stop],
      Bex.telemetry_measurements_base(%{}),
      %{result: result}
    )

    result
  end
end

I have released v0.3.0 which addresses naming in generated functions and/or :telemetry via Code.fetch_docs/1. Spec is what still bugs me, because I don’t yet see a legit non-hacky way to get to specs (although dialyzer does that and hence that’s possible :slight_smile:

dimitarvp

dimitarvp

Pretty nice, and impressive, but is there no way to name the function arguments by extracting them from the function definition somehow (sorry, not a pro in Erlang/Elixir meta-programming)?

I for one wouldn’t want my telemetry to have arg_1 and arg_2 etc. as parameters in the spans.

mudasobwa

mudasobwa

Creator of Cure

There are some pitfalls.

  • the function might have no named arguments
  • app source code is on hand, deps are too, to some extent, but functions defined in elixir core would require downloading and parsing the elixir sources
  • Erlang modules should also be treated differently.

In most cases parsing beam files should do though, and this feature is on my short-list.

Where Next?

Popular in Libraries Top

tmbb
I’ve been working on two packages (not on hex.pm yet) to build admin interfaces for phoenix apps: bureaucrat - which contains a bunch ...
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
kelvinst
Hey everyone! Well, we made this lib a while ago and now we decided to finally go out and public with it! It’s a tool for creating and m...
New
mbuhot
Leverage Open Api 3.0 (Swagger) to document, test, validate and explore your Plug and Phoenix APIs. Generate and serve a JSON Open API ...
New
riverrun
I’ve just released version 3 of Comeonin, a password hashing library. The following small changes have been made: changes to the NIF c...
New
Eiji
ExApi is a library that I’m developing now and hope release soon This library will allow to: list all apis list all api implementation...
New
wmnnd
Hi there, for my project DBLSQD, I needed a file storage solution that is a bit more flexible than Arc. Because I thought others might f...
New
gjaldon
As the title states, EctoEnum has just been updated after some time of hardly any activity in the repo. Here’s the latest release: https:...
New
handnot2
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application. This library uses Erlang esaml to provide plug enabl...
New
bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
381 12391 119
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

Sub Categories:

We're in Beta

About us Mission Statement