ProductPromotion
Logo

Elixir

made by https://0x3d.site

GitHub - bravobike/efx: A library to declaratively write testable effects
A library to declaratively write testable effects. Contribute to bravobike/efx development by creating an account on GitHub.
Visit Site

GitHub - bravobike/efx: A library to declaratively write testable effects

GitHub - bravobike/efx: A library to declaratively write testable effects

Efx

Tests Hex version badge

Testing with side-effects is often hard. Various solutions exist to work around the difficulties, e.g. mocking. This library offers a very easy way to achieve testable code by mocking. Instead of mocking we talk about binding effects to another implementation. Efx offers a declarative way to mark effectful functions and bind them in tests.

Efx allows async testing even in with child-processes, since it uses process-dictionaries to store bindings and find them in the supervision-tree (see this test-case).

Rationale

Efx is a small library that does one thing and one thing only very well: Make code that contains side effects testable.

Existing mock libraries often set up mocks in non-declarative ways: configs need to be adapted & mock need to be initialized. In source code there are intrusive instructions to set up mockable code. Efx is very unintrusive in both, source code and test code. It offers a convenient and declarative syntax. Instead of mocking we talk about binding effects.

Efx follows the following principles:

  • Implementing and binding effects should be as simple and declarative as possible.
  • Modules contain groups of effects that can only be bound as a set.
  • We want to run as many tests async as possible. Thus, we traverse the supervision tree to find rebound effects in the spawning test processes, in an isolated manner.
  • Effects by default execute their default implementation in tests, and thus, must be explicitly bound.
  • Effects can only be bound in tests, but not in production. In production, the default implementation is always executed.
  • We want zero performance overhead in production.

Usage

Setup

To use Efx in your project, add this to your dependencies in mix.ex:

{:efx, "~> 0.2.7"}

If you want to have proper formatting of the Efx.defeffect/2 macro, you can add the following line to your .formatter.ex:

[
  ...,
  import_deps: [:efx]
]

Example

Given the following code:

defmodule MyModule do

  def read_data() do
    File.read!("file.txt")
    |> deserialize()
  end

  def write_data(data) do
    serialized_data = data |> serialize()
    File.write!("file.txt", deserialized_data)
  end

  defp deserialize(raw) do
    ...
  end

  defp serialize(data) do
    ...
  end

end

In this example, it's quite complicated to test deserialization and serialization since we have to prepare and place the file correctly for each test.

We can rewrite the module using Efx as follows:

defmodule MyModule do

  use Efx

  def read_data() do
    read_file!()
    |> deserialize()
  end

  def write_data(data) do
    data
    |> serialize()
    |> write_file!()
  end

  @spec read_file!() :: binary()
  defeffect read_file!() do
    File.read!("file.txt")
  end

  @spec write_file!(binary()) :: :ok
  defeffect write_file!(raw) do
    File.write!("file.txt", raw)
  end

  ...

end

By using the defeffect-macro, we define an effect-function as well as provide a default-implementation in its body. It is mandatory for each of the effect-functions to have a matching spec.

The above code is now easily testable since we can rebind the effect-functions with ease:

defmodule MyModuleTest do

  use EfxCase

  describe "read_data/0" do
    test "works as expected with empty file" do
      bind(MyModule, :read_file!, fn -> "" end)
      bind(MyModule, :write_file!, fn _ -> :ok end)

      # test code here
      ...
    end

    test "works as expected with proper contents" do
      bind(MyModule, :read_file!, fn -> "some expected file content" end)
      bind(MyModule, :write_file!, fn _ -> :ok end)

      # test code here
      ...
    end

  end

end

Instead of returning the value of the default implementation, MyModule.read_file!/0 returns test data that is needed for the test case. MyModule.write_file! does nothing.

For more details, see the EfxCase-module.

Caution: Efx generates a behaviour

Note that Efx generates and implements a behavior. Thus, it is recommended, to move side effects to a dedicated submodule, to not accidentally interfere with existing behaviors. That said, we create the following module:

defmodule MyModule.Effects do

  use Efx

  @spec read_file!() :: binary()
  defeffect read_file!() do
    File.read!("file.txt")
  end

  @spec write_file!(binary()) :: :ok
  defeffect write_file!(raw) do
    File.write!("file.txt", raw)
  end

end

and straight forward use it in the original module:

defmodule MyModule do

  alias MyModule.Effects

  def read_data() do
    Effects.read_file!()
    |> deserialize()
  end

  def write_data(data) do
    data
    |> serialize()
    |> Effects.write_file!()
  end

  ...
end

That way, we achieve a clear separation between effectful and pure code.

Delegate Effects

The same way we use Kernel.defdelegate/2 we can implement effect functions that just delegate to another function like so:

@spec to_atom(String.t()) :: atom()
delegateeffect to_atom(str), to: String

delegateeffect follows the same syntax as Kernel.defdelegate/2. Functions defined using defdelegate are bindable in tests like they were created using defeffect.

OTP Version 25 required

The ancestor-key in process dictionaries is relativly new to Erlang. It was introduced with OTP 25 and, thus, this is the minimal required OTP-version.

License

Copyright © 2024 Bravobike GmbH and Contributors

This project is licensed under the Apache 2.0 license.

Articles
to learn more about the elixir concepts.

Resources
which are currently available to browse on.

mail [email protected] to add your project or resources here 🔥.

FAQ's
to know more about the topic.

mail [email protected] to add your project or resources here 🔥.

Queries
or most google FAQ's about Elixir.

mail [email protected] to add more queries here 🔍.

More Sites
to check out once you're finished browsing here.

0x3d
https://www.0x3d.site/
0x3d is designed for aggregating information.
NodeJS
https://nodejs.0x3d.site/
NodeJS Online Directory
Cross Platform
https://cross-platform.0x3d.site/
Cross Platform Online Directory
Open Source
https://open-source.0x3d.site/
Open Source Online Directory
Analytics
https://analytics.0x3d.site/
Analytics Online Directory
JavaScript
https://javascript.0x3d.site/
JavaScript Online Directory
GoLang
https://golang.0x3d.site/
GoLang Online Directory
Python
https://python.0x3d.site/
Python Online Directory
Swift
https://swift.0x3d.site/
Swift Online Directory
Rust
https://rust.0x3d.site/
Rust Online Directory
Scala
https://scala.0x3d.site/
Scala Online Directory
Ruby
https://ruby.0x3d.site/
Ruby Online Directory
Clojure
https://clojure.0x3d.site/
Clojure Online Directory
Elixir
https://elixir.0x3d.site/
Elixir Online Directory
Elm
https://elm.0x3d.site/
Elm Online Directory
Lua
https://lua.0x3d.site/
Lua Online Directory
C Programming
https://c-programming.0x3d.site/
C Programming Online Directory
C++ Programming
https://cpp-programming.0x3d.site/
C++ Programming Online Directory
R Programming
https://r-programming.0x3d.site/
R Programming Online Directory
Perl
https://perl.0x3d.site/
Perl Online Directory
Java
https://java.0x3d.site/
Java Online Directory
Kotlin
https://kotlin.0x3d.site/
Kotlin Online Directory
PHP
https://php.0x3d.site/
PHP Online Directory
React JS
https://react.0x3d.site/
React JS Online Directory
Angular
https://angular.0x3d.site/
Angular JS Online Directory