Kaigi on Rails 2023 現地参加して "E2E testing on Rails 2023" というお話をしてきた

幸いに登壇のチャンスがいただけたので、Kaigi on Rails 2023に現地参加してきました。

E2E testing on Rails 2023 by Yusuke Iwaki - Kaigi on Rails 2023

2年前の「システムテスト解剖学」という発表の続編的な位置づけで、RailsのE2Eテストを取り巻く技術の解説に徹する発表でした。

自分の発表のスタイルとして、「◯◯したらいいよ」「△△ライブラリ使うといいよ」系の発表にはしたくないというちょっとしたこだわりがあり、とはいえ今回の発表は手を抜くと「Playwright最高だよ、めっちゃいいよ」になってしまうので、注意深くストーリー展開を練って本番に望みました。

  • Node.jsベースのテストランナーを話す必然性
  • Railsで何をしたら幸せになれそうか?の定義
  • じつはあれもこれもしなくても、1個だけ仕組みを作ればいいんだという発見の導出

などなど、狙ったポイントについてはしっかり伝えられたかなーと思っています。

いっぽうで、DEMOのコンテンツの練習不足で、一部やることを忘れてしまうなどハプニングもありw、現地開催の面白さ・難しさも改めて実感しました。

(てか、そういえばリアル登壇これが初めてだよ自分...。Ginza railsもKaigi on Rails 2021もオンライン開催で自宅から話してたんだ。

Kaigi on Railsを引っ張る @okuramasafumi さん、および実行委員の皆様には本当に感謝です。Railsにゆかりのあるエンジニアが集まる場に参加して登壇できたことはエンジニアとして本当に至福の時間でした。

とりあえず登壇までにやったこと、当日のこと、などなどを忘れないように書き留めておこうと思います。(たぶん誰の参考にもならないw)

登壇ネタを決めるまで

今回は、おそらくかなりの倍率になろうかと思い、2つネタを出した。

たまたまE2E testingのほうが通ってしまった、というのが正直なところで、実はもう一個出していたネタ(Railsで作ってしまったものをRailsじゃなくSinatraへ移行する系のお話)のほうが渾身のネタではあった。

これはいずれどこかでまた発表するとして、まぁ世の中の期待というかお困りポイントとしてはE2Eテストなんだろなーという事情も察して、「E2E testing on Rails 2023」の準備を進めることにした。

登壇ネタの深掘り1 - Playwrightに詳しくなる

準備期間は2ヶ月もあるので、とりあえず最初にやったことは、Playwrightテストランナーのコンサルティング能力を上げること。中身を知り、どういうユースケースでどんな機能を使うべきか、詳しくなること。(はっきりいってRailsは何も関係がないw)

ただ、これをやったのにはいちおう理由も一応あって、Playwrightテストランナーは世の中のイケイケQAさん(いわゆるアーリーアダプター)がワッと使い始めているにすぎないフェーズで、もしかすると次の波がやってくるかもしれない。そうなるとPlaywrightテストランナーの何が凄かったのか?という話にもなる。つまり、現時点においてPlaywrightだけがすごい部分と、Node.jsベースのテストランナー全般としての利点と、べつに技術スタック関係ないけどPlaywrightでは利用しやすい機能と、など分けて考えておく必要がある。

Playwrightは公式の動画があり、開発してる人がかなりいろいろと紹介してくれているので、まずはそれを片っ端から見ていくことにした。

www.youtube.com

メモはこんなかんじでGoogleのメモに。

1.14から1.37まで、2年分のアップデートをとりあえず脳内に叩き込んで、次へ。

登壇ネタの深掘り2 - 必然性を感じられるストーリーの練り上げ

ここが結構時間かけたところ。Keynote触り始めるとそれだけで時間がかかってしまうので、今回もApple Pencilに投資をしてiPadでひたすら手書きメモで作っていった。

こんな感じで、図で説明すべき部分と、文章やチャートで説明すべき部分と、いろいろ脳内整理が捗った。

8割くらい作ったところで、なんかわけわからんくなって、雑記もまた色々w

仕事の方もそこそこ忙しかったので、空き時間でメモ、空き時間でメモ、空き時間でメモ、って感じでネタを作り上げていきました。

スライドの枚数的な進捗でいうと、8/21の登壇確定から10/21まで2ヶ月にわたり0枚ww

iPadでの脳内整理メモの効果により、60枚近いKeynote資料をほぼほぼ一晩で書き上げたのでしたw

登壇の反省

まず、なによりもDEMOの準備不足でした。焦って、VSCodeのPlaywright拡張機能の起動方法を忘れてしまうという大失態。

とはいえで、Playwrightの便利さが100%伝わらずとも、Node.jsのテストランナーが主流になってきているという事実づけから始めたストーリー展開により、DEMOの混乱が多少あっても、全体のストーリー理解には大きな支障が出なかったのが不幸中の幸い。

Shift + Command + Pでコマンドパレットを出すと、本来は Focus on Playwright viewが出て、うまくいくはずだったんだけど、なんか本番でやっても出なかったんだよなー・・・・必殺再起動が必要だったんだろうか・・・やっぱりわからんw

登壇発表のときに感じたこと

今回、現地の発表で、現場の様子を見ながら喋れるのが本当によかった。

2年前はオンライン開催で動画は事前収録だったこともあり、撮り直しができる最強のメリットはあったけど、やっぱり反響のないなかで喋り続けないといけない辛さも結構あった。

今回は、ONKさんらしき方が最前列でめっちゃウンウン聞いてくれているし、テスト書いてますか〜?みたいな挙手もリアルタイムにフィードバック得られるし、気分的にはかなり最高な発表だった。エンジニアリングのカンファレンスはこうじゃなくっちゃ!

発表資料

speakerdeck.com

ちょっとTwitterに共有するタイミングが速すぎて、直前の方の発表に埋もれてしまったのが反省。

サンプルコードなど

これずっと共有しわすれてましたw

DEMOサイトはこれ↓

github.com

Railsとして協調するために作らないといけない部分はこれ、ってところで紹介していたのは、いちおうGemにしていて、これ↓

github.com

ぜひ併せて見てほしい動画

Playwright作者の一人が、「なぜPlaywrightテストランナーを作ったのか」語っているものが、結構印象的です。

自動テスト関係なく、ぜひみんな見てみるとよいです。

www.youtube.com

まとめ

冒頭にも書いたように、Railsにゆかりのあるエンジニアが集まる場に参加して登壇できたことはエンジニアとして本当に至福の時間でした。また来年いけるよう準備するぞ

Rakuten miniを3000円で買って、みまもり端末にする

つくったもの

  • 学校にいるであろう時間帯は、アプリが何も使えない時計状態
    • でも親が本当に連絡を取りたいときに、電話はかかる(時計状態でも、着信画面は出る)
    • 動作確認してないが、たぶん地震速報とかそれ系もくる
  • スマホ使っていいよって時間帯は、限られたアプリだけ起動できる。

これなら「スマートフォン持ち込みなんて許さん、けど、みまもり端末に限って持ち込んでいいぞ」てきな学校を攻略できるんでは?そういう需要ありそうでは??

とか妄想してみたのであった。

つくりかた

  • 楽天ミニの中古デバイス
  • eSIM
  • 特定のアプリだけを表示するランチャーアプリ(自作する)
    • ManagedConfigurationを使って「お家モード」「学校モード」の切り替えができるように作るのがミソ

があればできる。

ランチャーアプリを作る

ランチャーと言っても、今回のユースケースでは任意のアプリを起動できる必要はない。

固定でアプリアイコンを配置してしまって、そのアイコンが押されたら特定のアプリ決め打ちでIntentを投げるだけのウルトラ適当実装で十分だ。

あとは、「お家モード」「学校モード」の2つのFragmentが切り替わるだけの1Activityのアプリを作ればいい。

ふつうにアプリを作る上ではあまり馴染みがないであろうRestrictionsManagerという機能を使い、MDMから流し込むことができる設定を定義・設定値の取得を行う。

Android Management APIでポリシーを作る

今回のアプリはAndroidバイス管理機能のKIOSKモードで動かす。(POSレジアプリとかでよく使われてるやつ)

ただ、大抵のMDMサービスを契約すると手間もお金もかかるので、簡易的なMDMサービスを自分で作ってしまうのがいいだろう。作り方は去年のアドベントカレンダーに結構細かく書いている。興味ある人は読んでくれ。

zenn.dev

で、MDMを作ってどういうポリシーを用意すればいいかというと、こんな感じ。「お家モード用のポリシー」「学校モード用のポリシー」の2つを用意し、お家モードで起動したいアプリは両方のポリシーにFORCE_INSTALLED指定で入れておく。

先の手順で作ったランチャーアプリはprivate appsとしてAndroid Management APIに登録し、以下のようにManaged Configurationを指定してインストール+KIOSKモードにする。

「お家モード用のポリシー」↓

  "applications": [
    {
      "packageName": "com.google.android.apps.maps",
      "installType": "FORCE_INSTALLED"
    },
    {
      "packageName": "com.google.android.apps.tachyon",
      "installType": "FORCE_INSTALLED"
    },
    {
      "packageName": "com.android.chrome",
      "installType": "FORCE_INSTALLED"
    },
    {
      "packageName": "dev.yusukeiwaki.mylauncher",
      "installType": "KIOSK",
      "managedConfiguration": {
        "mode": "home"
      }
    }
  ],

学校モードはmodeをschoolにしただけ。

      "packageName": "dev.yusukeiwaki.mylauncher",
      "installType": "KIOSK",
      "managedConfiguration": {
        "mode": "school"
      }
    }

あとは、適当な時間帯で、Android Management APIを叩いてポリシーを当てるようにすればいいだけ。IoTスイッチ的なものでポチッとできるようにするとさらに素敵かもしれない。

まとめ

Android Management APIがあれば専用端末作り放題で楽しい。

Rakuten miniは安くてコスパ最高

Ruby 2.7, 3.0, 3.1でもデータクラス (Data.define) が使えるようなライブラリdata_class_factoryを作った

まえおき

Ruby 3.2のデータクラスっていうやつがとてもハイセンス。実装した人自身が、dev.toでとても自慢げに記事を書いている。

dev.to

大昔に自分で初めて作ったライブラリが奇しくもデータクラスだったので、これは見逃せない。

yusukeiwaki.hatenablog.com

Ruby 3.2のデータクラスは、Structから機能を落とした版で、おそらく多くのRubyistたちが望んだものだろう。しかしながら当然のこと、これはRuby 3.1以前のバージョンでは使えない。

いっぽうで、そこそこ大きめのRailsアプリケーションを開発してると、Rubyのバージョンはそんなにホイホイ上げられるものではないので、Ruby 3.0や3.1止まりの会社も今はまだ多かろう。そんなわけで、kt_data_classのエッセンスは取り入れつつ、バックポートを目的としたライブラリを作ることにした。

1日で作りたい

kt_data_classを初めて作ったときは、たぶん1週間くらいかけて作ってたのだけど、今回のデータクラスのバックポートは、なる早で使いたい。

目標として「1日で仕上げる」ことを気に留めながら開発を進めた。

テスト駆動だが、今回はMinitest

スピード開発なので、迷わずテスト駆動で開発をすすめることにした。

kt_data_classのときは bundle gemのデフォルト値に従うままでRSpecでテストを書いていたが、Minitestの並列実行の速さに慣れてしまうとRSpecはとっても遅い。今回は速いMinitestを採用。

あと、bundle gemのデフォルトの構成だとtest_helperとか色々作られるが、よりシンプルな構成に変更。

Rakefileはこんなかんじ。 rake 単体でテストが流れるのは若干キモイので、 rake test でテストが流れるだけの設定にダイエット。

require 'bundler/gem_tasks'
require 'rake/testtask'

Rake::TestTask.new(:test) do |t|
  t.libs << 'test'
  t.libs << 'lib'
  t.test_files = FileList['test/**/*_test.rb']
end

個々のテストでは、test_helperを使わず、↓こんな感じのをテンプレートとしてこぴぺして使った。

require 'minitest/autorun'
require 'data_class_factory'

class XXXTest < Minitest::Test
  def test_zzzzzz

ここまでやることで、「テストファイル1つを見れば、単体テスト内容がすべてわかる」という状態にできる。あっちこっち見て回らなくて住むのでスピーディーにテストが書ける。

リファレンスとRuby本体の単体テストを大いに参考にした

Dataクラスについては、Rubyのリファレンスがとても充実している。

docs.ruby-lang.org

あとは、RubyにDataクラスが追加される際に書かれた単体試験(Add Data class implementation: Simple immutable value object by zverok · Pull Request #6353 · ruby/ruby · GitHub)も、ほぼそのまま流用することで、Dataクラスがどう振る舞うべきか、Ruby 3.2のDataクラスとの乖離が少ない形で実装が進められる。

dup, cloneと、Marshal.dump/loadの対応

Ruby本体の単体試験には以下のようなコードがある。

  def test_dup
    klass = Data.define(:foo, :bar)
    test = klass.new(foo: 1, bar: 2)
    assert_equal(klass.new(foo: 1, bar: 2), test.dup)
    assert_predicate(test.dup, :frozen?)
  end

  Klass = Data.define(:foo, :bar)

  def test_marshal
    test = Klass.new(foo: 1, bar: 2)
    loaded = Marshal.load(Marshal.dump(test))
    assert_equal(test, loaded)
    assert_not_same(test, loaded)
    assert_predicate(loaded, :frozen?)
  end

https://github.com/ruby/ruby/blob/v3_2_0/test/ruby/test_data.rb

シャローコピー(dupやclone)してもディープコピー(Marshal.dump/load)しても、freezeされた状態であれ、というもの。

class Data
  def initialize(**kwargs)
     ...いろいろ

     freeze
  end

こんな感じでinitializeの末尾でfreezeしておけばいいのだと思っていたが、これではdupやclone, Marshal.dump/loadいずれの操作時にも、freezeされていないインスタンスが生成されてしまった。

結論から言うと

  • dupやcloneでは #initialize_copy のオーバーライドが必要
  • Marshal.loadでは #marshal_dump #marshal_load のオーバーライドが必要
    • marshal_loadだけをオーバーライドしても動かない。両方オーバーライドが必要。

ということだった。kt_data_classのときはここまで考慮してなかったので、今回とても勉強になった。

github.com

.newの引数はキーワード引数だけにする

Structを踏襲してなのか、Dataクラスも「キーワード引数でもキーワード引数じゃなくてもいい」という仕様が健在だ。

Point = Data.define(:x, :y)

Point.new(3, 4) # OK
Point.new(x: 3, y: 4) # OK

ただ、これは完全に個人的意見だが、キーワード引数だけで十分だと思うし、そのほうが呼び出し側だけ読んで引数の意味がわかりやすくて良いと思う。

Feature #16122: Data: simple immutable value object - Ruby master - Ruby Issue Tracking Systemのディスカッションを見るに、どうもパターンマッチングとの整合性や、単純に書く量多くね?みたいな流れでキーワードじゃない引数もサポートするようにしたようだ。 また、New in Ruby 3.2 - Data.define - DEV Communityにも書かれているように、キーワードじゃない引数をサポートするための実装として、initializeではなく .newのほうで少々工夫が凝らされている。

もしキーワード引数だけの対応であれば、そんな工夫は一切いらない。

別にそれでよくね?

というわけで、バックポートDataクラスでは、一旦キーワード引数だけをサポートする形としてみた。

やっぱりpositional argumentもサポートしたほうが便利だなってなったら、今後追加するかもしれない。

で、一日でできたの?

思い立ったのが2023/3/4の10:53で

仕事終わりの夜に実装を進めて、データクラスが出来上がったのは 3/6の2:31 github.com

Gemを世に放ったのは3/8の10:28なので、まぁ全然1日では終わっていないw

そうはいっても、kt_data_classのときよりはだいぶスピーディーに開発したとは思う。(自画自賛w

まとめ

思い立って数日で、なんとか形にはした。

rubygems.org

そして、こういう既存機能ポーティングのライブラリ開発は作るたびに勉強になる。