From a9791d73db7fd9b7f26920ac475a17413bc62325 Mon Sep 17 00:00:00 2001 From: dmitrykleymenov Date: Thu, 12 Feb 2026 10:53:20 +0300 Subject: [PATCH 1/2] Translate code --- lib/module_resolver.ex | 10 ++-------- lib/module_resolver/mocks.ex | 4 ++-- lib/module_resolver/storage.ex | 2 +- lib/module_resolver/storage/app_env.ex | 2 +- test/module_resolver/mocks_test.exs | 6 ++++-- test/module_resolver/storage/app_env_test.exs | 10 +++++----- test/module_resolver_test.exs | 5 ++--- 7 files changed, 17 insertions(+), 22 deletions(-) diff --git a/lib/module_resolver.ex b/lib/module_resolver.ex index f87473c..254950c 100644 --- a/lib/module_resolver.ex +++ b/lib/module_resolver.ex @@ -1,6 +1,6 @@ defmodule ModuleResolver do @moduledoc """ - Библиотека для разделения(decoupling) зависимостей на уровне модулей с целью упрощения тестирования + A library for decoupling dependencies at the module level to simplify testing. """ defmodule InvalidOptionError do @@ -46,9 +46,6 @@ defmodule ModuleResolver do args = Macro.generate_arguments(arity, env.module) |> Macro.escape() spec = type_spec |> Macro.escape() - # quote теперь используется только непосредственно для inject'a кода - # Только 2 вещи происходят в контексте модуля behaviour: - # добавление спеки и определение функции. Всё остальное происходит в контексте ModuleResolver quote bind_quoted: [implementation: implementation, fun_name: fun_name, args: args, spec: spec] do Elixir.Kernel.@(spec(unquote(spec))) @@ -59,8 +56,7 @@ defmodule ModuleResolver do end end - @spec implementation(behaviour_module(), fallback_impl :: implementation_module()) :: - implementation_module() + @spec implementation(behaviour_module(), fallback_impl :: implementation_module()) :: implementation_module() def implementation(behaviour, fallback_impl) do Storage.get_implementation_module(behaviour) || fallback_impl end @@ -72,8 +68,6 @@ defmodule ModuleResolver do if compile_only_default?() do default_impl else - # Здесь так же не обойтись без контекста caller'a, так как - # проверять какой сервис дернуть необходимо в момент вызова имплементируемой функции quote do unquote(__MODULE__).implementation(unquote_splicing([implementation_module, default_impl])) end diff --git a/lib/module_resolver/mocks.ex b/lib/module_resolver/mocks.ex index 929c033..5e26f01 100644 --- a/lib/module_resolver/mocks.ex +++ b/lib/module_resolver/mocks.ex @@ -1,12 +1,12 @@ defmodule ModuleResolver.Mocks do @moduledoc """ - Модуль хранящий всю функциональность связанную с моками + The module stores all the functionality associated with mocks. """ alias ModuleResolver.Storage defmodule MockFactory do @moduledoc """ - Поведение для генератора моков + The behaviour for mock generator. It should have `defmock/1` function to define mocks """ @type t :: module() diff --git a/lib/module_resolver/storage.ex b/lib/module_resolver/storage.ex index 339ea37..c3a7835 100644 --- a/lib/module_resolver/storage.ex +++ b/lib/module_resolver/storage.ex @@ -2,7 +2,7 @@ defmodule ModuleResolver.Storage do alias ModuleResolver.Storage.AppEnv @moduledoc """ - Модуль behaviour для хранения имплементаций в runtime + Behaviour module for storing implementations at runtime """ @type t :: module() diff --git a/lib/module_resolver/storage/app_env.ex b/lib/module_resolver/storage/app_env.ex index bd05313..36f016c 100644 --- a/lib/module_resolver/storage/app_env.ex +++ b/lib/module_resolver/storage/app_env.ex @@ -1,6 +1,6 @@ defmodule ModuleResolver.Storage.AppEnv do @moduledoc """ - Модуль имплементации ModuleResolver.Storage с использованием Application environment + ModuleResolver.Storage implementation module using application environment """ @behaviour ModuleResolver.Storage diff --git a/test/module_resolver/mocks_test.exs b/test/module_resolver/mocks_test.exs index 6409309..66dad07 100644 --- a/test/module_resolver/mocks_test.exs +++ b/test/module_resolver/mocks_test.exs @@ -31,7 +31,7 @@ defmodule ModuleResolver.MocksTest do ModuleResolver.Mocks.defmocks([TestBehaviour1, TestBehaviour2], mock_factory: Mox) end - test "defines mocks for behaviours with given postfix" do + test "defines mocks for behaviours with the given postfix" do expected_implementations = %{ TestBehaviour1 => ModuleResolver.MocksTest.TestBehaviour1NotAMock, TestBehaviour2 => ModuleResolver.MocksTest.TestBehaviour2NotAMock @@ -44,8 +44,10 @@ defmodule ModuleResolver.MocksTest do end defp expect_storage_put_implementations(expected_implementations) do + count = Enum.count(expected_implementations) + StorageMock - |> expect(:put_implementation_module, 2, fn behaviour, mock -> + |> expect(:put_implementation_module, count, fn behaviour, mock -> assert behaviour in Map.keys(expected_implementations) assert mock == expected_implementations[behaviour] end) diff --git a/test/module_resolver/storage/app_env_test.exs b/test/module_resolver/storage/app_env_test.exs index d24057f..ea1bd92 100644 --- a/test/module_resolver/storage/app_env_test.exs +++ b/test/module_resolver/storage/app_env_test.exs @@ -9,14 +9,14 @@ defmodule ModuleResolver.Storage.AppEnvTest do end describe "get_implementation_module/1" do - test "returns nil if there are no implementations stored" do - refute AppEnv.get_implementation_module(__MODULE__) - end - - test "returns implementation from storage" do + test "returns an implementation from storage" do :ok = AppEnv.put_implementation_module(__MODULE__, TestModule) assert AppEnv.get_implementation_module(__MODULE__) == TestModule end + + test "returns nil if there are no implementations stored" do + refute AppEnv.get_implementation_module(__MODULE__) + end end describe "put_implementation_module/2" do diff --git a/test/module_resolver_test.exs b/test/module_resolver_test.exs index 91d3deb..8a8cae4 100644 --- a/test/module_resolver_test.exs +++ b/test/module_resolver_test.exs @@ -4,8 +4,7 @@ defmodule ModuleResolverTest do import Mox - # Здесь необходимо записать в env переменную отрицательный флаг `компиляции только имплементации по умолчанию` - # для проверки основных сценариев + # To prevent direct compilation of the default implementation, the `compile_default_impl` option must be set to false. Application.put_env(:module_resolver, :compile_default_impl, false) defmodule TestBehaviour do @@ -22,7 +21,7 @@ defmodule ModuleResolverTest do @callback increment(integer()) :: integer() end - # Возвращаем стандартное поведение при котором происходит выбор имплементации при компиляции + # We return the option value `true` to check for these cases. Application.put_env(:module_resolver, :compile_default_impl, true) defmodule TestOnlyDefaultBehaviour do From 6a56749d7de4c52f10f4f147609517e72f5b0b95 Mon Sep 17 00:00:00 2001 From: dmitrykleymenov Date: Thu, 12 Feb 2026 15:11:17 +0300 Subject: [PATCH 2/2] Translate readme --- README.md | 115 +++++++++++++++++++++++++----------------------------- 1 file changed, 53 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 145d445..0653857 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,22 @@ # ModuleResolver -Библиотека для разделения(decoupling) зависимостей на уровне модулей с целью упрощения тестирования. Так же предоставляет удобный интерфейс для создания моков этих зависимостей. +A library for decoupling module-level dependencies to simplify testing. It also provides a convenient interface for creating mocks of these dependencies. -## Добвление `module_resolver`'a +## Installation -Добавляем библиотеку в `mix.exs` файл: +Add `:module_resolver` to the list of dependencies in `mix.exs`: ```elixir def deps do [ - ..., {:module_resolver, "~> 0.1.0"} ] end ``` -## Использование в модулях +## Usage -Для того чтобы иметь возможность в тестах подменять модуль моками необходимо в модуле с колбеками использовать `use ModuleResolver`, например: +You may use `ModuleResolver` in a module that is expected to be mocked. For example: ```elixir defmodule MyModule @@ -33,42 +32,42 @@ defmodule MyModuleDefaultImplementation def some_function(counter), do: {:ok, counter} end ``` +Here `MyModule` uses `ModuleResolver` and specifies the default implementation module through `default_impl` option. After defining a callback in the behaviour module and the function itself in the implementation module, the call to `MyModule.some_function/1` will be delegated to the `MyModuleDefaultImplementation.some_function/1`. -Параметр `default_impl` передает имя модуля, который будет использоваться по умолчанию. Например, результатом вызова `MyModule.some_function(5)` будет результат вызова `MyModuleDefaultImplementation.some_function(5)`. - -Параметр `default_impl` можно опустить: +To define mocks in tests you may do this in the `test_helper.exs` file: ```elixir -defmodule MyModule - use ModuleResolver + ModuleResolver.Mocks.defmocks([MyModule], mock_factory: Mox, postfix: "Mock") +``` - @callback some_function(integer()) :: {:ok, integer()} +The first agrument here is a list of behaviour modules and the second one is `options`. There are two possible options: - defmodule DefaultImpl - @behaviour MyModule - - @impl true - def some_function(counter), do: {:ok, counter} - end -end -``` +- `mock_factory`, implementation of `BeamMetrics.Mocks.MockFactory` behaviour. It can be `Mox` or `Hammox` as well. +- `postfix`, will be added to the end of the behaviour module name to create a mock module name. Can be omitted. By default: `"Mock'` -В таком случае модуль по умолчанию будет формироваться из неймспейса модуля(в нашем случае `MyModule`) и `DefaultImpl`. Т.е в примере выше это будет `MyModule.DefaultImpl`. Это удобно для быстрого разделения зависимостей в существующих модулях. -Например, имеем изначально стандартный модуль: +To use these mocks, you need to disable implementation mounting at compile time. Add the following to the `test.exs` file: ```elixir +config :module_resolver, compile_default_impl: false +``` + +Now call to `MyModule.some_function/1` in test environment wil be delegated to `MyModuleMock.some_function/1`. In other environments the module under `default_impl` will be compiled into behaviour. + +## Compile-time/runtime + +By default, implementation is compiled into the behaviour module, and after compilation, the result roughly looks like: + +```Elixir defmodule MyModule - @spec some_function(integer()) :: {:ok, integer()} - def some_function(counter), do: {:ok, counter} + def some_function(count), do: MyModuleDefaultImplementation.some_function(count) end ``` -Для того чтобы использовать его Mock в тестах нам необходимо сделать всего 4 небольших шага: -1. Добавить `use ModuleResolver` -2. Существующие спеки перенести в callback'и -3. Все определения функций завернуть в дополнительный модуль `DefaultImpl` -4. В `DefaultImpl` прописать `@behaviour MyModule` в начало и `@impl true` у каждой функции -В итоге получаем: +When `compile_default_impl: false` is set, the implementation is determined at runtime. If there is no mock for a given behavior, a default implementation will be used, so integration tests can use the actual implementation without defining mocks. + +## Other use cases + +The `default_impl` option may be omitted: ```elixir defmodule MyModule @@ -85,48 +84,42 @@ defmodule MyModule end ``` -## Использование в тестах - -После того как модули были настроены, можно использовать их моки в тестах. Для этого необходимо в `test_helper.exs` указать список модулей первым параметром и двумя опциями: -- `mock_factory` с помощью которой будут созданы моки -- `postfix`, который будет добавлен к названию модуля behaviour чтобы получить имя mock модуля +In this case default implementation will be set as `__MODULE__.DefaultImpl`. In the code above it will be `MyModule.DefaultImpl`. It's helpful when you need to decouple existed modules. For example, we have a module such as: ```elixir - ModuleResolver.Mocks.defmocks([MyModule, MyAnotherModule], mock_factory: Mox, postfix: "Mock") +defmodule MyExistedModule + @spec existed_function(integer()) :: {:ok, integer()} + def existed_function(counter), do: {:ok, counter} +end ``` -В результате этой команды будут созданы моки `MyModuleMock` и `MyAnotherModuleMock`, которые будут автоматически вызываться при каждом вызове любой функции из модулей `MyModule` и `MyAnotherModule` соответственно. - -С помощь обязательной опции `mock_factory` необходимо передать модуль генерации моков. Модуль генерации должен имплементировать поведение `ModuleResolver.Mocks.MockFactory`. Можно так же использовать `Mox` или `Hammox`. -Опцию `postfix` можно опустить, тогда она по умолчанию будет равна `Mock` +To use `MyExistedModuleMock` instead of `MyExistedModule` in tests, you need to follow 5 steps: -## Конфигурация +1. Add `use ModuleResolver` +2. Replace `@spec` with `@callback`. +3. Wrap all function definitions in the `DefaultImpl` module +4. Add `@behaviour MyExistedModule` to the top of the `DefaultImpl` module and `@impl true` to each function. +5. Add `MyExistedModule` to the mocks list in `ModuleResolver.Mocks.defmocks/2` -По умолчанию модуль имплементации будет выбираться в момент компиляции, при этом на этом этапе функции имплементации будут "вмонтированы" в функцию поведения. В итоге получится нечто вроде: +As result: ```elixir -defmodule MyModule - use ModuleResolver, default_impl: MyModuleDefaultImplementation - @callback some_function(integer()) :: {:ok, integer()} - - def some_function(count), do: MyModuleDefaultImplementation.some_function(count) -end -``` - -Относительно решенения без `module_resolver`a overhead составляет лишь 1 дополнительный вызов функции в стек вызовов. +defmodule MyExistedModule + use ModuleResolver -Использование модулей в тестах требует runtime определения модуля имплементации, для этого необходимо сконфигурировать `module_resolver`, передав параметр `compile_default_impl: false`. Например, в конфиг `test.exs` добавить: + @callback existed_function(integer()) :: {:ok, integer()} -```elixir -config :module_resolver, compile_default_impl: false + defmodule DefaultImpl + @behaviour MyExistedModule + + @impl true + def existed_function(counter), do: {:ok, counter} + end +end ``` -В таком случае поведение будет выбираться каждый раз в момент вызова функции из модуля behaviour - ## Benchmarking -Сравнение времени выполнения на примере следующих модулей: - ```elixir defmodule BenchTestsBehaviour do use ModuleResolver, default_impl: BenchTestsImplementation @@ -165,7 +158,7 @@ Benchee.run( ) ``` -Результаты при настройке конфигурации `config :module_resolver, compile_default_impl: false`: +Results with `config :module_resolver, compile_default_impl: false`: ```bash Name ips average deviation median 99th % @@ -183,7 +176,7 @@ implementation direct call 59.52 MB behaviour call 65.63 MB - 1.10x memory usage +6.10 MB ``` -Результаты при настройке конфигурации `config :module_resolver, compile_default_impl: true`: +Results with `config :module_resolver, compile_default_impl: true`: ```bash Name ips average deviation median 99th % @@ -199,6 +192,4 @@ Memory usage statistics: Name Memory usage implementation direct call 59.52 MB behaviour call 59.52 MB - 1.00x memory usage -0.00018 MB - -**All measurements for memory usage were the same** ``` \ No newline at end of file