ナガモト の blog

Full Cycle Developerを目指すエンジニアが有用そうな技術記事や、ポエムのようなよしなしごとを投稿するブログです。

Ruby on Rails でDB構造を扱うRidgepole gemにコントリビュートしました

業務でよくお世話になっているRidgepoleというgemにPRを出してマージされたので、その経緯を書き残しておきます。

コントリビュータの証

Distinguish same foreign key constraint on multiple columns by nagamoto · Pull Request #278 · winebarrel/ridgepole · GitHub f:id:ngmt83:20190718161748p:plain Contributors to winebarrel/ridgepole · GitHub

どや!(なおdiffはたった1行)

Ridgepoleとは?

Rails標準のマイグレーションの代替となるgemです。

github.com

複数人・チームで並列して開発を行うとデータ変更が競合したり、内容は競合していなくともschema.rbのversion部分(後述)でやたらとコンフリクトが発生します。そういった問題を解消してくれます。

ActiveRecord::Schema.define(version: 2019_07_18_0000000)

詳しくはこちらのクッ社開発ブログをご覧ください。

techlife.cookpad.com

コントリビュートに至る経緯

現職で扱うRailsアプリが巨大になり、並列する開発でDB周りのコンフリクト発生が増えてきたため、私が利用経験のあるRidgepole導入を試しはじめました。

最初に次のコマンドでDBからSchemafile作成を行いました。

bundle exec ridgepole -c config/database.yml --export --output db/Schemafile

そして作成されたSchemafileを次のコマンドでapplyしました。

bundle exec ridgepole -c config/database.yml --apply -f db/Schemafile

すると次のようなエラーが発生しました。

[ERROR] Foreign Key `xxx(["xxx", "yyy"])` already defined

このエラーを解消するために調査した結果、コントリビュートするのがシンプルで良い解決方法だと判断し、PRを作成しました。

(Schemafileに外部キー制約が重複して宣言されているというエラーだったため、デフォルト外部キー制約名を付与するオプション--dump-with-default-fk-nameを利用してもエラーが発生しました。)

エラーの再現

わかりやすくエラーを再現したほうがPRの有効性を説明しやすいと考え、シンプルなRailsアプリでエラーとなるケースを再現しました。

ユーザ間でダイレクトメッセージを送信できるアプリケーションを想定します。ERDは次の次の通り。

再現から分析

詳細はPRに書いてありますが、いずれもusersテーブルからdirect_messagesテーブルへの複数の外部キー制約を正しく区別できずに、重複した定義だとしてエラーになっていました。

外部キー制約が重複していないかチェックしている箇所はこちらです。

idx = options[:name] || [from_table, to_table]

raise "Foreign Key `#{from_table}(#{idx})` already defined" if @__definition[from_table][:foreign_keys][idx]

見た限りでは外部キー制約はその名前、もしくは制約を設ける2つテーブル名によってユニークであるか否かを判定しているようです。実際の動かしてもそのように挙動であることが確認できました。

変更を加える

コードリーディングとエラーの再現から、ほぼ間違いないレベルで改修する箇所を特定できたため、変更を加えて次のようにしました。

idx = options[:name] || [from_table, to_table, options[:column]]

raise "Foreign Key `#{from_table}(#{idx})` already defined" if @__definition[from_table][:foreign_keys][idx]

[from_table, to_table, options[:column]]により、同じ2テーブル間に複数の制約があってもカラムが指定されていればそれらが異なる制約だと区別できるようになりました。

PRを作る

英語で作成しました。正直この英文にかなり時間がかかりました。

Distinguish same foreign key constraint on multiple columns by nagamoto · Pull Request #278 · winebarrel/ridgepole · GitHub

失礼のないように、わかりやすいようにと丁寧に書いたおかげかスムーズにマージしてもらえたので非常に嬉しかったです。winebarrelさんありがとうございます。

最後に

自分には難しいと思っているかもしれませんが、やってみたらOSSのオーナーは優しいし、サクサクことが進むこともあります。 ぜひみなさんもOSSにコントリビュートしましょう!

起業家本田圭佑氏の求人noteから使用技術を想像する

IT業界でも動向を注目されている本田圭佑氏がエンジニアを募集する記事を投稿しました。

note.mu

彼のイメージを良い意味で裏切られるとても王道な良い記事だと思いました。(語彙が豊富ではないためエモいとしか言えません。)

  • 親近感が湧くような失敗を含めたエピソード
  • いたずらな横文字を使わず、わかりやすいビジョン・バリューの説明
  • 強力なボードメンバー*1
  • 成長中毒者というキャッチーな表現

これらは求人noteを書くことがあれば参考にしたいですね。

本題

noteの最下部にエントリー用google formがあったため、使用技術*2を想像してみたいと思います。

※全編想像・妄想の域をでませんのでその前提で読んでください。

f:id:ngmt83:20190711192324p:plain
業務経験入力欄

最も特徴的なのは Dart

Dartの記載からFlutterを使用する可能性がまずまず高いと思います。 そうでもなければ絞り込まれた言語(技術)の欄にDartは入らないと思います。

スマホアプリがネイティブでない可能性

iOS, Androidと記載されていますが、Swift、Kotlinとは書かれていません。 これが意図的なものだとすると、FlutterによるiOS, Android両対応アプリの可能性もあるでしょう。

採用人数の記載はありませんでしたが、少数であればFlutterの可能性は高まりそうです。 (iOS, Kotlinそれぞれの優秀なエンジニアを揃えるのは簡単ではないため)

早い開発速度実現のためのスクリプト言語

PHP, Rubyの記載があるあたりに開発速度を重視していそうです。実際に採用する言語は集まったメンバー次第で選ぶくらいではないかと想像しています。

スポーツマッチングサービスNow Doを考えた時にパフォーマンスを要求される場面はリリース初期にはあまりなさそうに感じるため、スクリプト言語をサーバーサイドに採用する可能性は高そうです。*3 sportsbull.jp

汎用的なJava

エンジニアの母数が多く、パフォーマンスや堅牢さを考慮するとJavaが記載されるのは自然です。Androidネイティブな側面も含めるとJava経験者に求める能力は多岐に渡りそうです。

しかし実際に最初期のサーバーサイドにJavaを使うのかどうかは疑問です。パフォーマンス・堅牢さを優先するならJava以外にも選択肢がある上、開発速度を優先するスクリプト言語採用の可能性のほうが高いためです。*4

機械学習系の記載なし

最初期であるため記載がないだけかもしれませんが、サーバーサイドにも使用できるPythonの記載がないのは少し驚きでした。 Now Doや他のサービスをリリースしてから、成長してからでもよいと考えているのでしょうか。

総合的にみて

集まったメンバーのスキルセットに依存するとは思いますが、最初期は次のような構成になるのではないでしょうか。

  • サーバー: Rails(Laravel)製APIサーバー
  • スマホアプリ: Flutter or Swift or Kotlin or PWA (iOSのみ先行リリースもありそう)
  • WEB: Flutter Web or ReactなどのSPA

そりゃそうだろという感じの月並みな予想ですみません。 個人的な希望ですが、FlutterとFlutter Webの両採用という攻めた構成を少し見たい気がしています。

フォーム全体の所感

使用技術以外については、有名どころのエージェントやサービスでよく求められる内容でした。 奇をてらうことなく王道をいくのは本田圭佑氏にしては手堅いとは思いましたが、その方がいいよなとも思います。

最後に

起業家本田圭佑氏がどう動くか、どんなサービスができるのか楽しみです。

*1:名村さん強い 1000人体制でもエンジニアが成長する組織とは? 海外事例から学ぶテックキャリアの拡げ方【メルカリ CTO・名村卓】 - エンジニアtype | 転職type

*2:まだ確定していない可能性もある

*3:FWはRailsかLaravelあたりか

*4:スクリプト言語でカバーできない箇所を他の言語で・・・という方針であれば、NodeやGoあたりか

2019年4~6月振り返り

2019年ももう半分ですよ、新元号令和も2ヶ月が経ちましたよ。時の流れは残酷ですが、振り返っていきます。

振り返りの手法はびば(森のフレンズ) (@viva_tweet_x) | Twitterさんが行なっているYWTを参考にしています。*1

3月分までは振り返り記事を書いていたので4~6月分の振り返りです。(振り返りサボんな) ngmt83.hatenablog.com

Y: やったこと

  • 習慣化完了
    • 週1記事ブログ投稿
    • 毎日運動
  • write-blog-every-weekに参加
  • Glide appsによるアプリ作成*2*3
  • アプリ開発進行中
  • 現職で大きなプロジェクトほぼ終わり
  • 転職活動本格化
  • 健康診断結果大幅改善

ブログは2019年皆勤賞、運動は体調不良時の嫁ストップ時のみのさぼりです。習慣化が完了したと言っていいでしょう。ブログはwrite-blog-every-weekに入れてもらってより続けやすい体制が整い、身内みたいなものですがブログを見てくれる人が増えたのは嬉しいことです。アプリ開発も粛々と進めています。

仕事では育休から復帰後初の繁忙期(大きなプロジェクトが佳境)を迎え、真に仕事・ブログ・子育てすべて同時期にコミットできるかを試されました。その時期に転職活動まで被せてしまったため想像以上の多忙さですが、なんとかやれています。とはいえ慢性的な寝不足は感じているため、てきぱきやって睡眠時間を確保することが課題です。

ダイエットの結果がでたことも非常に嬉しいです。健康診断前の絶食(勘違いして20時間ほど食事を抜いてしまった)も手伝って64kg台を記録しました。2018年の最大体重は74kgだったのでとてもいい感じです。

W: わかったこと

  • 「ブログを書く時間はある」*4を経験として実感
  • 意識を強く持たなければ記事の質が上がらない
  • 多忙な時期の転職活動は判断を誤る可能性が高い
  • 繁忙期は容易にリバウンドする

ブログを書くことはできていますが、質は上がらないままです。書評は特に全然進みません。

また、先ほどダイエットうまくいってると書きましたが、6月はリバウンドしました。ストレスは食べて解消するタイプなので繁忙期に最大68kgまでまで戻ってしまいました。食事以外の気軽なストレス解消方法か、ローカロリーなストレス解消食を見つける必要がありそうです。

T: つぎにやること(7~9月)

  • 習慣の再構築
  • アプリリリース
  • ダイエット再開

挑戦することを減らし、習慣の再構築とアプリ開発により集中する予定です。習慣アプリで体重計測・運動・ブログは計測しやすく続いていますが、継続できていないものもあるため、以下の通りに見直します。

  • 体重計測(毎日)
  • 運動(毎日)
  • 学習(読書1ページ or アプリ開発1コミット。毎日)
  • 早寝(25:00までに就寝。毎日)
  • 健康的な食事(高カロリーな間食・おかわりを避ける。週5日以上)
  • ブログ(7日に1回以上)

早寝というには遅すぎるとか、健康的な食事というよりは太らないように心がける程度だとかまあ甘い習慣ではありますが、続けることが大事なので最初のハードルは低くいきます。

習慣に関する参考記事

kakakakakku.hatenablog.com

細かいこと考えたりしてますが、要はやるんだよ!やっていき!という感じです。

*1:twitter.com

*2:ngmt83.hatenablog.com

*3:

*4:https://speakerdeck.com/kakakakakku/passion-for-blog-mentoring?slide=43

Railsアプリのマスターデータ管理 Seed Fu ベタープラクティス

Railsアプリ開発におけるマスターデータの扱い方は大きく次の3つです。

  1. 標準機能のseedを利用する
  2. migrationを利用する
  3. その他

選択肢で言うとその他になりますが、私は複数の現場で導入してきたのSeed FuというGemをおすすめしています。この記事ではSeed Fuを利用する際のベタープラクティスを紹介します。

github.com

おすすめの記法

シンタックスシュガーとして用意されている下記の記法を用いましょう。マスターデータは基本的に静的な値を取り扱うもので、テーブルにレコードとして挿入されることがわかりやすいこの記法がおすすめです。Seed Fuを全く知らない人もすぐ理解してくれます。*1

都道府県のマスターデータを定義する例

Prefecture.seed(:id,
  { id: 1,  name: "北海道" },
  { id: 2,  name: "青森県" },
  { id: 3,  name: "岩手県" },
# ~中略~
  { id: 47, name: "沖縄県" }
)

開発環境用に大量のデータをループで挿入したいときなど、向かないと判断した時は他の記法を利用しましょう。ここでは説明しません。素直にREADMEを読みましょう。 github.com どうしても日本語情報がいい人は以下の記事を参照ください。 qiita.com

変更頻度に合わせた書き方

ほぼ確実に変更がありえず、誤って変更されることを防ぎたい場合はseed_onceで書きましょう。

Prefecture.seed_once(:id,
  { id: 1,  name: "北海道" },
  { id: 2,  name: "青森県" },
  { id: 3,  name: "岩手県" },
# ~中略~
  { id: 47, name: "沖縄県" }
)

こうするとid 1~47で存在しないものにのみinsertが行われるようになります。 (seedの場合は内容が異なるレコードは更新してくれます)

そもそもDBに保存するということは永続化したいけど変更もしたいマスターデータではないのか?という話もあります。プログラム内に定数で持つことも可能ですが、RDBに入れておくとBIツールでデータのみを参照する際にもわかりやすいといった利点もあります。 結局はケースバイケースです。その場に応じた判断をしましょう。

依存関係にあるマスターデータ挿入順序対策

マスターデータで外部キーを持つような依存関係になるものもあるでしょう。例えば都道府県と地方のような関係です。 Seed Fuはファイル名のアルファベット順で実行します*2*3。それを利用して、ファイルのプリフィックスに数字をつけましょう。一番最初に実行する必要があるファイルのプリフィックスは0です。私は同時実行で問題ないファイルも同じ数字を設定します。

地方のマスターデータを定義ファイル(00_areas.rb)

Area.seed_once(:id,
  { id: 1,  name: "北海道地方" },
  { id: 2,  name: "東北地方" },
# ~中略~
  { id: 8, name: "九州地方" }
)

都道府県のマスターデータ定義ファイル(01_prefectures.rb)

Prefecture.seed_once(:id,
  { id: 1,  name: "北海道", area_id: 1 },
  { id: 2,  name: "青森県", area_id: 2 },
  { id: 3,  name: "岩手県", area_id: 2 },
# ~中略~
  { id: 47, name: "沖縄県", area_id: 8 }
)

※二桁数字を採用しているのは開発用テストデータと合わせてファイル数が多くなることがあるためです。*4

同一ファイル内に書くことでより直感的に順序を制御する方法もあります。とはいえ、データが増えた時に見通しが悪くなったり、検索性が下がったりするため、私は1modelに対して1seedファイルを作成しています。

Seed Fu Gemを意識せずに実行させる

通常のrails db:seedコマンド実行時にSeed Fuが呼び出されるように設定しましょう。

db/seeds/seeds.rb

SeedFu.seed

標準的な方法ではrails db:seed_fuという簡単ですが独自コマンドが必要です。しかし、前述のように標準のseed実行時に呼び出す設定をすることで開発メンバー全員がGem特有のコマンドを覚えずにすみます。

また、この設定をすると標準のseedで管理していたものをSeed Fuに切り替えるときにも、CIやDeployの設定変更が不要になります。 (もともとseedを実行する設定になっているはずのため)

自動テスト実行前に一度だけマスターデータを挿入する

自動テストは副作用をなくす意味でもDBがクリーンな状態で実行されます。*5しかし、マスターデータについては存在することを前提に自動テストを実行したいはずです。 RSpecによる自動テストの場合、実行前に一度だけSeedFu.seedを実行するように次の記述をします。

spec/spec_helper.rb (rails_helper.rbの場合もある)

RSpec.configure do |config|
  config.before(:suite) do
    SeedFu.seed
  end
end

自動テストにおいて副作用をより厳密になくすため、Database Cleanerを合わせて利用する場合はこちらの記事がとても参考になります。 blog.inouetakuya.info

Seed Fuを用いたマスターデータ管理ベタープラクティスまとめ

  • シンタックスシュガーを利用したわかりやすい記法で書く
  • 変更頻度を意識し、seedseed_onceを使い分ける
  • ファイル名に実行順序を表すプリフィックスとして数字をつける
  • db/seeds/seeds.rbにSeedFu.seedと記述して標準のrails db:seedで実行可能にする
  • 自動テスト実行前にSeedFu.seedが実行されるように設定する

*1:当社比

*2:公式で明言されている仕様のため、gemのupdateによる挙動変更はなさそうです

*3:https://github.com/mbleigh/seed-fu/blob/master/README.md#where-to-put-seed-files

*4:開発用テストデータでは、1つのユーザの操作において複数のレコードが決まった順番で作成されることがある。そのときには10_xx.rb, 11_yy.rbとすることで10番台を1つの操作とみなしてまとめたりします

*5:テストで挿入されたデータはテスト終了後に削除する(される)のが一般的

Classi Angular Night #3 参加レポート

先日参加したこちらの勉強会のレポートです。 classi-angular-night.connpass.com ハッシュタグのTLからツイートを抜粋しながらざっくり振り返ります。

イベント概要

本イベントでは、Angularのプラクティス活用事例、Tipsなどを共有できればと思っています。

日本国内ではReactやVueほど活発ではないので、少し貴重なAngularの勉強会です。 (私はAngularが好き)

会場提供はビズリーチさんでした。遅れてきてもスムーズに入館できて助かりました。

発表内容

HRMOSにおけるAngularのエラーハンドリングについて

発表者: 杉原 碧志さん (株式会社ビズリーチ)

私は途中参加であまり理解できていませんでしたが、タイトルの通りAngularにおけるエラーハンドリングのプラクティスが詰まった発表だったようです。

会場の声(TLから抜粋)

Deep dive into Component

発表者: 笠原 渉さん (Classi株式会社)

初〜中級者向けの内容だと感じました。なんとなくで使ってしまいがちなライフサイクルフックについて分かりやすく説明されていました。 また、アプリが大きくなったときに困り始めるであろうパフォーマンスに優しい書き方も、具体的な対策込みで説明されていて理解がはかどりました。

会場の声(TLから抜粋)

Upgrade Angular to v8.0

発表者: lacolaco さん

先日リリースされたAngular v8での差分をメインに、Angularのリリーススケジュール・アップグレード方法・deprecatedから削除までの流れなど、Angularのアップグレードについてまとめられた発表でした。 まだアップグレード経験が浅い私には非常にありがたかったです。

会場の声(TLから抜粋)

tsconfig.jsonのcompilerOptionsにstrict: trueを追加した話

発表者: しみきょん さん

具体的な取り組み方も説明されていて、型宣言をおろそかにしがちな私のモチベーションをあげてくれる素晴らしい発表でした。

会場の声(TLから抜粋)

はじめて業務でAngularを使って苦労したこと

発表者: 川上 和義さん

資料は見つけられませんでした。*1

「ng-japanスタッフだけど業務でAngular使ったことなかった」という川上さんが実際に0からアプリを作る中で得た知見が詰まった発表でした。 要領よく要所を解説し、Angular""業務"では"使ってなかっただけで、フロントエンドには多くの知見を持っている方ということがひしひしと伝わってきました。

会場の声(TLから抜粋)

LT枠① Angularチュートリアル2週目を一瞬でやる (筆者のうろ覚えです)

発表者: almirajさん

資料はありませんでしたが、ライブチュートリアルを通してlacolacoさんの言っていたチュートリアルの重要さを体現してくれました。

20190327 - Classi Angular Night #2 - Google スライド

会場の声(TLから抜粋)

LT枠② Our Track to Modern Angular #2

www.slideshare.net

AngularJS時代からのアプリとどう付き合うか、Angular7.2とのハイブリッドまでどうやって行き着いたかという発表でした。 Angular4以降の整備されたぬるま湯にだけ浸かってきた私は難しさを想像することもできませんでした。

会場の声(TLから抜粋)

感想

また、次のツイートから察するに毎回神回でおすすめということのようです。

そういえば

公式にイベントレポートがありました。こっちも見ましょう! www.wantedly.com

*1:上がってたら追記するので@ngmt83に教えてください

AngularアプリにJsBarcodeでバーコードを表示する

↓先日投稿した記事↓の続きです。 ngmt83.hatenablog.com

AngularでJsBarcodeというモジュールを使う方法を紹介します。

github.com

AngularからJsBarcodeを使用する方法(基本)

GitHub - lindell/JsBarcode: Barcode generation library written in JavaScript that works in both the browser and on Node.js

ここに書いてある通りで非常に簡単ですが、Angularに組み込むための手順があるので軽く説明します。

  1. npm install (yarn add)
  2. 使用するcomponentでimport
  3. バーコードを表示するためのhtml要素を用意
  4. html要素にJsBarcodeを設定

これだけです。簡単ですね。(なのにちょっとハマりました)

サンプルをこちらにデプロイしています。各手順も公開しているコードを添えておきます。

Demo

npm install (yarn add)

npm install jsbarcode

説明するまでもなく、モジュールを使うときはたいていこれですね。yarnの人はyarn addです。

使用するcomponentでimport

import * as JsBarcode from 'jsbarcode';

https://github.com/nagamoto/demo/blob/master/src/app/barcode/barcode.component.ts#L3

Generate barcode on Angular2 project · Issue #134 · lindell/JsBarcode · GitHubを参考にimportしました。

バーコードを表示するためのhtml要素を用意

<svg id=fixedBarcode></svg>

https://github.com/nagamoto/demo/blob/master/src/app/barcode/barcode.component.html#L3

今回はsvg形式で表示するため、svgタグを定義しました。また、バーコードを設定する際にセレクタが必要なのでidを指定しています。

html要素にJsBarcodeを設定

JsBarcode('#fixedBarcode', 'fixedBarcode', {});

https://github.com/nagamoto/demo/blob/master/src/app/barcode/barcode.component.ts#L20

第1引数にバーコードを表示する要素を特定するためのセレクタを指定し、第2引数にバーコード化する値を指定します。 componentが初期化される際にバーコードが表示されて欲しいので、ngOnInitフックに記述しています。

軽くハマったポイント

  • 動的ページ構築(*ngIfなど)でのNoElementException
  • id属性の先頭が数字

動的ページ構築(*ngIfなど)でのNoElementException

アプリを作っていれば、必要なときだけ表示したりしなかったりするのは日常茶飯事です。Angularでは動的にページ要素を構築する場合に ngIf, ngFor, ngSwitchCase などを使います。こういった実装を行うと、タイミングによっては条件判定と要素の描画がなされていないためにJsBarcode('#fixedBarcode', 'fixedBarcode', {});実行時にセレクタに該当する要素がないという主旨の次のエラーが発生します。

NoElementException: No element to render on.

対策として、前述のエラーを避けるためにライフサイクル・フック*1を適切に使い分けました。安易によく使用するngOnInitを利用せず*2、適切なタイミングに設定しましょう。*3今回は*ngIfの判定が終わり、要素が描画されたタイミングです。そのためngAfterViewInitでバーコード描画処理を実行しています。

https://github.com/nagamoto/demo/blob/master/src/app/barcode/barcode.component.ts#L25

id属性の先頭が数字

id属性の先頭が数字だと、正常に動作しません。そもそも先頭が数字のid属性は認められていないので当然です。List構造で複数のUUID(のような値)をバーコードで表示する際に、バーコードを表示するhtml要素のid属性をユニークにするため次のようにUUIDを流用し、この問題に遭遇しました。

html例

<svg [id]="uuid" class="barcode"></svg>

ts例

JsBarcode(`#${this.uuid}`, this.uuid, {});

UUIDは数字を含むため、たまに数字が先頭の文字列になります。そのときのみエラーが発生します。先頭が数字のid属性は認められていないというのは初歩的な話ですがうっかりしていました。対策として適当なプリフィックスをつけて解決しました。当たり前ですがidは先頭が数字になる可能性がないか確認しましょう。

最後に

Angularはv8もリリースされ、とても便利なのでぜひ使ってみてください。私はv9でIvyが正式リリースされるのが楽しみです。

*1:https://angular.jp/guide/lifecycle-hooks

*2:自分に言い聞かせています

*3:Angularライフサイクル・フックの参考資料: https://qiita.com/tomonari-t/items/3fd6d3c30b6b007b0f14

Railsアプリ開発におけるテスト戦略 〜オレオレベタープラクティス〜

いきなりですが、戦術・戦略という言葉を正しく理解していますか?大雑把でもいいのでどう違うのか、どちらがより抽象度が高く・具体性が低いのか理解しておきましょう。

ざっくり言うと、戦略は大局的な目的や方策のことであり、戦術は戦略を実現するための具体的な手段や計画のことです。*1*2

大切なのは、戦略が間違っていてはどんなに優れた戦術を用いても大きく状況を改善するのは難しいということです。*3

ソフトウェアのテストにおいても、具体的な書き方・ルールの整備など優れた戦術を取っているにも関わらず、そもそもの戦略を間違えているがばかりに状況が改善しないことがあります。当記事はソフトウェアテスト(特にRailsRSpecの場合)における戦略を紹介します。

Railsアプリ開発におけるテスト戦略について語る前によくある課題と戦術、それから発生する問題を説明します。

テストコードによくある課題

  • テストが不足している
  • テストを書くコストが高い
  • テストを読む難易度が高い

これらの問題は表裏一体で繋がっています。そもそもテストを読む難易度が高いがために、書くコストが高くなり、コストが高いためにテストを書けずに不足してしまいます。

(RSpecで)とりがちな戦術

  • letは遅延評価なので冒頭ですべて定義しておく
  • よく使用するデータセットをテスト実行前に入れておく
  • FactoryBotで汎用性の高いデータを定義して使い回す
  • 定義済みレコードにupdateをかけて使い回す
  • shared_xxでまとめて定義して使い回す
  • オリジナルな記法・メソッドを定義して多数のexpectaion記述を端折る
  • eachやwhileなどのループで記法で多パターンのテストを行う

大抵は書く量を減らして読みやすくしたり、書きやすくしようという狙いで採用されます。*4 これらの戦術は用法用量を守って用いれば薬になるでしょうが、毒になる場合もあります。また、そもそもの戦略が間違っている場合、戦術レベルの対応では状況はじわじわと悪くなる一方です。

発生しがちな問題

  • 仕様変更やリファクタリングでshared_xxなど共通化した箇所が軒並み使えなくなる
  • 通化されているため、ある特定のケースにのみ、テスト項目を増やすことが難しい
  • 複数ファイルを行き来しなければテストの動作を把握できない
  • テストコードの複雑度が上がり、テストコードをテストする必要性を感じ始める

ではこういった状況に陥らないためのオレオレベタープラクティスを説明していきます。

Railsアプリ開発におけるテスト戦略 〜オレオレベタープラクティス〜

ベタープラクティスは次の流れで行います。

  • テストによりどんな課題を解決したいか確認する
  • ソフトウェアテストについての基礎基本を開発メンバー全員に理解してもらう
  • E2Eテスト, Controllerテスト, Modelテスト各レイヤの落とし所を決める
  • 戦略についてチームで共通認識を作る
  • 戦術についてチームに浸透させる

テストによりどんな課題を解決したいか決める

私はRailsアプリを開発する際にはスピーディな開発改善サイクルを回すことが重要だと思っています。そのためには高い頻度でリリースをしたいです。しかし、自動テストなしではデグレ発生が恐ろしく頻繁なデプロイはできません。そのため、解決したい課題は次の通りです。

「品質の担保を容易にし、安全に高い頻度でリリースを行えるようにする」

この課題について何を今更と思う人は多いでしょう。しかし継続的に開発していく中でテストを書くという行為がただの作業になってしまうことはたまにあります。そのような状態では非効率的なテストを書いてしまったり、実際にはありえないような意味のないテストを書いてしまいます。そんなときはそもそもの目的・課題に立ち返りましょう。

ソフトウェアテストについての基礎基本を開発メンバー全員に理解してもらう

ソフトウェアテストについて学んだことはありますか?「単体テスト結合テストホワイトボックステストブラックボックステスト・分岐網羅・条件網羅・境界値分析」これらの意味はわかるでしょうか?

私は大学・大学院で研究のためにソフトウェアテストの基礎基本を学びました。*5意識の低い学生でしたが、そこで学んだ知識はテストを設計するときに非常に役立っています。テストは経験則で自然と書いたり設計したりできるようになると思われがちですが、やはり基礎基本を抑えるとテストの勘所を掴むのが早くなり、効率よくどんどん良いテストを設計できるようになります。

ja.wikipedia.org

E2Eテスト, Controllerテスト, Modelテスト各レイヤの落とし所を決める

まず大前提として、すべてのレイヤで網羅率100%となるようなテストを行うのは現実的ではありません。そのため、基本的には品質の高いテストで効率よくバグを検出する方針をとることになります。

ソフトウェアテストの一分野、組み合わせテストでは「大多数のバグは少数因子の値の組み合わせで検出できる」と知られています。*6*7ソフトウェアは多数のモジュール(因子)の組み合わせとみなすことができるため、小さい各モジュール毎の単体テストを厚く行うことが比較的少ないテストで多数のバグを検出できるよいテスト方針となります。そもそも、複数のモジュールを組み合わせたテストを設計するのと、モジュール毎にテストを設計するのでは後者の方が簡単ということも単体テストを厚く行う方針の追い風です。

単体テストなどのソフトウェアテスト用語をRailsに対応させると次の通りです。

つまり、Modelテストを厚く行うことが効率の良い品質の高いテストになります。しかしながら、各レイヤ毎にしか検証できない項目や低い割合とはいえ組み合わせたときにのみ検出されるバグもあるため、Controllerテスト・E2Eテストも行わないというのはよくないと考えます。

理論とこれまでの経験も踏まえた結果として、私がよく利用する落とし所はこれです。

「Modelテストをほぼすべてのメソッドを検証する程度に厚く、 Controllerテストを正常系・準正常系のレスポンスをそれぞれ1パターン以上、E2Eテストをサービスにおけるクリティカルな機能*8について行う」

※時々のリソースや状況次第です

戦略についてチームで共通認識を作る

当然ですがテストコードもアプリケーションコードと同じく全開発メンバーが書くでしょう。あるメンバーはControllerテストのみを厚く書き、Modelテストを書かない。別のメンバーはModelテストしか書かない。そのような状況では品質の高いテストが実行できているとは言えません。

テスト戦略について決めているのであれば共有しましょう。チーム全員で決めて納得感を醸成することもいいでしょう。(少人数チームに限る)

戦術についてチームに浸透させる

戦略を共有し、同じ粒度のテストを書いていても、それぞれが自由きままに勝手にテストコードを書いていてはカオスになるでしょう。RailsというFWを用いてアプリケーションコードについては規約を重んじ、統一感のある書き方をしている。そんなエンジニアであれば、テストコードでも統一感ある書き方をした方がよいということはすぐ理解できるでしょう。

RSpecを用いたテストコードの書き方については次の記事に参考資料をまとめてあります。チームが大きくなるときまでには、そのチーム合った書き方・規約を大まかにでも定められるといいでしょう。 ngmt83.hatenablog.com

まとめ

Railsアプリ開発におけるテスト戦略 〜オレオレベタープラクティス〜」は、

  • ソフトウェアテストの基礎基本を大まかに理解する
  • 「Modelテストはほぼすべてのメソッドを検証する程度に厚く、 Controllerテストは正常系・準正常系をそれぞれ1パターン以上、E2Eテストはサービスにおけるクリティカルな機能についてのみ行う」
  • チームが大きくなるまでにRSpecの書き方・規約を大まかに定める
  • テストの目的・戦略を定期的に見つめ直す

これらをチームメンバー全員に共有・浸透させる。

です。

*1:「戦略」と「戦術」 - 違いがわかる事典

*2:3分でわかる孫子の兵法/戦略!戦いを避けながら、弱者が強者に勝つ | ビジネススキル大全 | ダイヤモンド・オンライン

*3:大阪から東京へ向かう際に、西へ進むという戦略ではどんなに早い乗り物(優れた戦術)を用いても東京にたどり着くために莫大なコストがかかる。

*4:他にはテスト実行時間が長く、オーバーヘッドを減らすためという狙いもある。

*5:ディペンダビリティ(高信頼性)を専門とする研究室でソフトウェアテストにおけるテストケースの自動設計について研究してました。

*6:出典論文: Software fault interactions and implications for software testing - IEEE Journals & Magazine

*7:組み合わせテストの日本語参考記事: 組み合わせテストの用語「2因子間網羅」「直交表」「All-Pairs法」 - Qiita

*8:例: ECサービスにおけるカートに入れて購入完了まで。マッチングサービスにおけるマッチング申請、承認、マッチング成立まで。ユーザ新規登録から完了まで。etc