Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

     

Elixir Circuits.I2C with Mox

This is written in Japanese. I might convert it to English later, maybe.

はじめに

Elixirのテストでモックを用意するときに利用するElixirパッケージとして、moxが人気です。Elixir作者のJosé Valimさんが作ったからということもありますが、ただモックを用意するだけではなくElixirアプリの構成をより良くするためのアイデアにまで言及されているので、教科書のようなものと思っています。

一言でいうと「その場しのぎのモックはするべきではない」ということです。モックが必要となる場合はまず契約(behaviour)をしっかり定義し、それをベースにモックを作ります。結果としてコードの見通しもよくなると考えられます。そういった考え方でのモックを作る際に便利なのがmoxです。

しかしながら、moxの設定方法は(慣れるまでは)あまり直感的ではなく、おそらく初めての方はとっつきにくそうな印象を持つと思います。自分なりに試行錯誤して導き出した簡単でわかりやすいmoxの使い方があるので、今日はそれをご紹介させていだだこうと思います。いろいろなやり方があるうちの一例です。

例として、circuits_i2cを用いて温度センサーと通信するElixirコードのモックを考えてみます。

Elixirのリモートもくもく会autoracexでおなじみのオーサムさん(@torifukukaiou)が以前こうおっしゃってました。

原典をあたるが一番だとおもいます。
原典にすべて書いてある。

まずはJosé Valimさんの記事ドキュメントを一通り読んで頂いて、その上で戸惑った際の一助になれば幸いです。

依存関係

  • moxをインストール。
  • 契約をしっかり定義するためには、それ以前にがちゃんと定義されている必要があります。ですので理想としてはdialyxirで型チェックした方が良いと個人的には考えてます。
# mix.exs    ...    defp deps do      [        ...+       {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},+       {:mox, "~> 1.0", only: :test},        ...      ]    end    ...
Enter fullscreen modeExit fullscreen mode
$ cd path/to/my_app$ mix deps.get
Enter fullscreen modeExit fullscreen mode

Circuits.I2C

  • I2C通信するのに便利なElixirパッケージ。
  • 例えば、ElixirアプリからI2Cに対応するセンサーと通信するのに使える。
  • センサーが相手のアプリで、どのようにしてセンサーの無いテスト環境でアプリをイゴかせるようにするかが課題。

Circuits.I2Cに定義されている関数を確認してみます。これらの関数でセンサーを用いて通信します。

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\\[])
Enter fullscreen modeExit fullscreen mode

以降でCircuits.I2Cをモックに入れ替えできように工夫して実装していきます。

behaviourを定義する

  • まずは「データ転送する層」の契約を定義します。どう定義するかは任意です。
  • 例として、個人的に気に入っているパターンを挙げます。
# lib/my_app/transport.exdefmoduleMyApp.Transportdodefstruct[:ref,:bus_address]## このモジュールで使用される型@typet::%__MODULE__{ref:reference(),bus_address:0..127}@typeoption::{:bus_name,String.t()}|{:bus_address,0..127}## このbehaviourの要求する関数の型@callbackopen([option()])::{:ok,t()}|{:error,any()}@callbackread(t(),pos_integer())::{:ok,binary()}|{:error,any()}@callbackwrite(t(),iodata()):::ok|{:error,any()}@callbackwrite_read(t(),iodata(),pos_integer())::{:ok,binary()}|{:error,any()}end
Enter fullscreen modeExit fullscreen mode

behaviourを実装する(本番用)

  • 実際のセンサーに接続することを想定した実装。
  • 記述量が比較的少ない場合、僕は便宜上behaviourの定義と同じファイルにまとめることが多いです。
# lib/my_app/transport.exdefmoduleMyApp.Transport.I2Cdo@behaviourMyApp.Transport@implMyApp.Transportdefopen(opts)dobus_name=Access.fetch!(opts,:bus_name)bus_address=Access.fetch!(opts,:bus_address)caseCircuits.I2C.open(bus_name)do{:ok,ref}->{:ok,%MyApp.Transport{ref:ref,bus_address:bus_address}}{:error,reason}->{:error,reason}endend@implMyApp.Transportdefread(transport,bytes_to_read)doCircuits.I2C.read(transport.ref,transport.bus_address,bytes_to_read)end@implMyApp.Transportdefwrite(transport,data)doCircuits.I2C.write(transport.ref,transport.bus_address,data)end@implMyApp.Transportdefwrite_read(transport,data,bytes_to_read)doCircuits.I2C.write_read(transport.ref,transport.bus_address,data,bytes_to_read)endend
Enter fullscreen modeExit fullscreen mode

behaviourを実装する(スタブ)

  • センサーがない環境での実行を想定した実装。
  • モックの基本的な振る舞いを定義。
  • モック自体は空のモジュールで、スタブがモックに振る舞いを与えるイメージ。
  • 引数はarityさえあっていればOK。
  • behaviourの型に合う正常系の値を返すようにする。
  • 記述量が比較的少ない場合、僕は便宜上behaviourと同じファイルにまとめることが多いです。
  • どうしてもtest配下に置きたい場合は、test/supportを用意するやり方moxのドキュメントに紹介されてます。
# lib/my_app/transport.exdefmoduleMyApp.Transport.Stubdo@behaviourMyApp.Transport@implMyApp.Transportdefopen(_opts)do{:ok,%MyApp.Transport{ref:make_ref(),bus_address:0x00}}end@implMyApp.Transportdefread(_transport,_bytes_to_read)do{:ok,"stub"}end@implMyApp.Transportdefwrite(_transport,_data)do:okend@implMyApp.Transportdefwrite_read(_transport,_data,_bytes_to_read)do{:ok,"stub"}endend
Enter fullscreen modeExit fullscreen mode

モックのモジュールを準備する

  • テスト用にモックのモジュールを準備する。
  • MyApp.Transportの実装(: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()
Enter fullscreen modeExit fullscreen mode

MyApp.Transportを用いてアプリを書いてみる

例えば温度をセンサーのから読み込むGenServerを書くとこんな感じになります。

# lib/my_app.exdefmoduleMyAppdouseGenServer@typeoption()::{:name,GenServer.name()}|{:bus_name,String.t()}@specstart_link([option()])::GenServer.on_start()defstart_link(init_arg\\[])doGenServer.start_link(__MODULE__,init_arg,name:init_arg[:name])end@specmeasure(GenServer.server())::{:ok,MyApp.Measurement.t()}|{:error,any()}defmeasure(server),do:GenServer.call(server,:measure)@implGenServerdefinit(config)dobus_name=config[:bus_name]||"i2c-1"# ここでモジュールを直書きしないこと!casetransport_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)}")endend...# 動的にモジュールを入れ替えする関数。# これをモジュール属性(@transport_mod)として定義してしまうとコンパイル時に固定されてしまう# ので注意が必要です。関数にしておくとが実行時に評価されるので直感的で無難と思います。defptransport_mod()doApplication.get_env(:my_app,:transport_mod,MyApp.Transport.I2C)endend
Enter fullscreen modeExit fullscreen mode

モックを用いてテストを書いてみる

  • import Moxmoxの関数を使えるようになります。
  • setupはおまじないです。
# test/my_app_test.exsdefmoduleMyAppTestdouseExUnit.CaseimportMoxsetup:set_mox_from_contextsetup:verify_on_exit!setupdo# モックにスタブをセットする。これでセンサーがなくてもコードがイゴくようになります。Mox.stub_with(MyApp.MockTransport,MyApp.Transport.Stub):okend...
Enter fullscreen modeExit fullscreen mode
# test/my_app_test.exstest"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:_}=measurementendtest"measure when read failed"doMyApp.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
Enter fullscreen modeExit fullscreen mode

今回ご紹介したパターンはAHT20のElixirパッケージでバリバリ活躍しています。

以上!
🎉🎉🎉

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

たのしむ
  • Location
    🇯🇵
  • Work
    ソフトウエアエンジニア
  • Joined

More fromMasatoshi Nishiguchi

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp