불로장생약 회로.I2C Mox 포함
はじめに
Elixir のテストでモックを用意するときに利用する Elixir パッケージとして、 mox が人気です. Elixir 作者の さんが作ったからということもありますが、ただモックを用意するだけではなく Elixirアプリの構成をより良くするためのアイデア にまで言及されているので、教科書のようなものと思っています.
一言でいうと「その場しのぎのモックはするべきではない」ということです.モックが必要となる場合はまず契約( behaviour )をしっかり定義し、それをベースにモックを作ります.結果としてコードの見通しもよくなると考えられます.そういった考え方でのモックを作る際に便利なのが mox です.
しかしながら、 mox の設定方法は(慣れるまでは)あまり直感的ではなく、おそらく初めての方はとっつきにくそうな印象を持つと思います.自分なりに試行錯誤して導き出した簡単でわかりやすい mox の使い方があるので、今日はそれをご紹介させていだだこうと思います.いろいろなやり方があるうちの一例です.
例として、 circuits_i2c を用いて温度センサーと通信する Elixir コードのモックを考えてみます.
Elixir のリモートもくもく会 autoracex でおなじみのオーサムさん(
)が以前こうおっしゃってました.
原典をあたるが一番だとおもいます。
原典にすべて書いてある。
まずは José Valimさんの記事 と ドキュメント を一通り読んで頂いて、その上で戸惑った際の一助になれば幸いです.
依存関係
mox をインストール.
# mix.exs
...
defp deps do
[
...
+ {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
+ {:mox, "~> 1.0", only: :test},
...
]
end
...
$ cd path/to/my_app
$ mix deps.get
전기 회로.I2C
I2C 通信するのに便利な Elixir パッケージ.
Circuits.I2C.open(bus_name)
Circuits.I2C.read(i2c_bus, address, bytes_to_read, opts \\ [])
Circuits.I2C.write(i2c_bus, address, data, opts \\ [])
Circuits.I2C.write_read(i2c_bus, address, write_data, bytes_to_read, opts \\ [])
以降で Circuits.I2C
をモックに入れ替えできように工夫して実装していきます.비헤이비어を定義する
# lib/my_app/transport.ex
defmodule MyApp.Transport do
defstruct [:ref, :bus_address]
## このモジュールで使用される型
@type t ::
%__MODULE__{ref: reference(), bus_address: 0..127}
@type option ::
{:bus_name, String.t()} | {:bus_address, 0..127}
## このbehaviourの要求する関数の型
@callback open([option()]) ::
{:ok, t()} | {:error, any()}
@callback read(t(), pos_integer()) ::
{:ok, binary()} | {:error, any()}
@callback write(t(), iodata()) ::
:ok | {:error, any()}
@callback write_read(t(), iodata(), pos_integer()) ::
{:ok, binary()} | {:error, any()}
end
비헤이비어を実装する(本番用)
# lib/my_app/transport.ex
defmodule MyApp.Transport.I2C do
@behaviour MyApp.Transport
@impl MyApp.Transport
def open(opts) do
bus_name = Access.fetch!(opts, :bus_name)
bus_address = Access.fetch!(opts, :bus_address)
case Circuits.I2C.open(bus_name) do
{:ok, ref} ->
{:ok, %MyApp.Transport{ref: ref, bus_address: bus_address}}
{:error, reason} ->
{:error, reason}
end
end
@impl MyApp.Transport
def read(transport, bytes_to_read) do
Circuits.I2C.read(transport.ref, transport.bus_address, bytes_to_read)
end
@impl MyApp.Transport
def write(transport, data) do
Circuits.I2C.write(transport.ref, transport.bus_address, data)
end
@impl MyApp.Transport
def write_read(transport, data, bytes_to_read) do
Circuits.I2C.write_read(transport.ref, transport.bus_address, data, bytes_to_read)
end
end
비헤이비어を実装する(スタブ)
behaviour の型に合う正常系の値を返すようにする.
test
配下に置きたい場合は、 test/support
を用意するやり方 が mox のドキュメントに紹介されてます.# lib/my_app/transport.ex
defmodule MyApp.Transport.Stub do
@behaviour MyApp.Transport
@impl MyApp.Transport
def open(_opts) do
{:ok, %MyApp.Transport{ref: make_ref(), bus_address: 0x00}}
end
@impl MyApp.Transport
def read(_transport, _bytes_to_read) do
{:ok, "stub"}
end
@impl MyApp.Transport
def write(_transport, _data) do
:ok
end
@impl MyApp.Transport
def write_read(_transport, _data, _bytes_to_read) do
{:ok, "stub"}
end
end
モックのモジュールを準備する
:transport_mod
)を入れ替えできるようにする. :transport_mod
をモック( MyApp.MockTransport
)にしておく.# test/test_helper.exs
+ Mox.defmock(MyApp.MockTransport, for: MyApp.Transport)
+ Application.put_env(:aht20, :transport_mod, MyApp.MockTransport)
ExUnit.start()
나의 응용.수송하다を用いてアプリを書いてみる
例えば温度をセンサーのから読み込む GenServer を書くとこんな感じになります.
# lib/my_app.ex
defmodule MyApp do
use GenServer
@type option() :: {:name, GenServer.name()} | {:bus_name, String.t()}
@spec start_link([option()]) :: GenServer.on_start()
def start_link(init_arg \\ []) do
GenServer.start_link(__MODULE__, init_arg, name: init_arg[:name])
end
@spec measure(GenServer.server()) :: {:ok, MyApp.Measurement.t()} | {:error, any()}
def measure(server), do: GenServer.call(server, :measure)
@impl GenServer
def init(config) do
bus_name = config[:bus_name] || "i2c-1"
# ここでモジュールを直書きしないこと!
case transport_mod().open(bus_name: bus_name, bus_address: 0x38) do
{:ok, transport} ->
{:ok, %{transport: transport}, {:continue, :init_sensor}}
error ->
raise("Error opening i2c: #{inspect(error)}")
end
end
...
# 動的にモジュールを入れ替えする関数。
# これをモジュール属性(@transport_mod)として定義してしまうとコンパイル時に固定されてしまう
# ので注意が必要です。関数にしておくとが実行時に評価されるので直感的で無難と思います。
defp transport_mod() do
Application.get_env(:my_app, :transport_mod, MyApp.Transport.I2C)
end
end
モックを用いてテストを書いてみる
import Mox
で mox の関数を使えるようになります. setup
はおまじないです.# test/my_app_test.exs
defmodule MyAppTest do
use ExUnit.Case
import Mox
setup :set_mox_from_context
setup :verify_on_exit!
setup do
# モックにスタブをセットする。これでセンサーがなくてもコードがイゴくようになります。
Mox.stub_with(MyApp.MockTransport, MyApp.Transport.Stub)
:ok
end
...
# test/my_app_test.exs
test "measure" do
# 各テストでexpectを用いて具体的にどの関数がどのようにして何度呼ばれることが「期待されるか」を指定。
MyApp.MockTransport
|> Mox.expect(:read, 1, fn _transport, _data ->
{:ok, <<28, 113, 191, 6, 86, 169, 149>>}
end)
assert {:ok, pid} = MyApp.start_link()
assert {:ok, measurement} = MyApp.measure(pid)
assert %MyApp.Measurement{
humidity_rh: 44.43206787109375,
temperature_c: 29.23145294189453,
timestamp_ms: _
} = measurement
end
test "measure when read failed" do
MyApp.MockTransport
|> Mox.expect(:read, 1, fn _transport, _data ->
{:error, "Very bad"}
end)
assert {:ok, pid} = MyApp.start_link()
assert {:error, "Very bad"} = MyApp.measure(pid)
end
今回ご紹介したパターンは AHT20のElixirパッケージ でバリバリ活躍しています.以上!
🎉🎉🎉
Reference
이 문제에 관하여(불로장생약 회로.I2C Mox 포함), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/mnishiguchi/elixir-circuitsi2c-with-mox-3186텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)