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.4.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:

Last Post!

mudasobwa

mudasobwa

Creator of Cure

✓ proper typespec inherited from the original function

Package published to bex | Hex
(786bc96c074d4f06b10861610c26114e4f0fbdf33a6e4bd4223e8fced3f37676)

Where Next?

Popular in Announcing Top

grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 54006 488
New
jakub-zawislak
Hi everyone, I’m coming from the Symfony (PHP) framework. I like Phoenix, but it has a one thing that was build much better in the Symfo...
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
kip
Image is an image processing library for Elixir. It is based upon the fabulous vix library that provides a libvips wrapper for Elixir. I...
622 19460 194
New
wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
Flo0807
Hello everyone! I am excited to share our heart project Backpex with you. After building several Phoenix applications, we realized that...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
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 31494 112
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
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement