はじめに
今年の5月〜6月の2か月間で、Redmineの用語集プラグインを一から再実装する作業をしてきた結果、RubyとRuby on RailsとRedmineプラグインの作り方についてやっとある程度分かるようになってきました。
まだ当初目標はコンプリートしていませんが(テストの途中で立ち往生気味)、その模様は次のWikiにあります。
Redmine Glossaryプラグイン再構築 - ソフトウェアエンジニアリング - Torutk
7月に入ったので、本来の目的であった用語集プラグインをRedmine 4.0(Rails 5.2)で動作するよう修正する作業に入りました。こちらは1週間でほぼ作業を終えました。この修正作業のまとめを次のブログに記載しました。
Redmine 4.0(Rails 5.2)に用語集(Glossary)プラグインを対応させる - Qiita
修正作業の過程は次のチケットにメモを残しています。
機能 #81: Redmine 4.0でglossary pluginを動くようにする - ソフトウェアエンジニアリング - Torutk
今後、用語集プラグインのメンテナンスをできるようにするためには、構造を分かりやすくするリファクタリングをしていきたいので、そのためにはテストが不可欠となります。
Railsのテスト
MinitestかRSpecか
Rails標準のテストは、Minitestと呼ばれるテストフレームワークを使ったものです。しかし、Redmineプラグイン開発やRailsの開発関係の記事などでは、別途RSpecを推奨しているものを多く見かけます。Minitest v.s. RSpec を扱うブログも多数見つかります。
Railsを使ったプログラム(Redmienプラグイン)の開発知識を得るために買った次の書籍でも、テストの章ではRSpecを「デファクトスタンダードである」として採用しています。
Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法 (Web Engineer's Books)
- 作者: 太田智彬,寺下翔太,手塚亮,宗像亜由美,株式会社リクルートテクノロジーズ
- 出版社/メーカー: 翔泳社
- 発売日: 2018/01/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
2015年の関西Ruby会議06での次のセッション資料では、「両方使ってみて判断する」とまとめられています。
RSpecとMinitest使うならどっち
テストコードの書き方(API)については、Minitestはassertを使ったテスト判定を行うので、CppUnitやJUnit(バージョン3まで)などのユニットテストフレームワークで用いられているassertによるテスト判定に慣れていると障壁を感じることなくMinitestのAPIに馴染めます。
RSpecは内部DSLで独特の書き方を覚える必要があります。JUnit 4で導入されたmatcherに概念が近いですがAPIはJUnit 4とは違う印象です。
テストデータの準備については、Minitestの特徴であるfixturesを使った、テストデータをYAMLで記述しテスト実行時に読み込みテストコードで利用する方法と、RSpecではFactoryBotでテストデータを生成する方法となり、分かりやすさは圧倒的にfixturesです。しかし、モデルのテストではデータベースの制約・整合性テストをする必要があり、異常値(制約違反データ)をデータベースに入れてわざとエラーを発生させることがfixturesでは出来ないため、RSpec(FactoryBot)がいいという話も見かけました。
モックについてはRSpecに軍配が上がる(Minitestにはモックがない?)ようでうs。
ということで、どちらを選ぶかは、やはりトレードオフとなるようです。
今回はトレードオフの主項目を、学習障壁が低いこと(環境構築の手間と覚えることが少ないこと)とし、テストコードが増えてもよしとし、Minitestを使って進めることとします。
RubyやRailsで覚えることだらけの現状では、RSpecの書き方をさらに覚えるのはアップアップ気味で辛さを感じてしまいます。
モデルのテストについて
そもそもモデルクラスをどう構成するか
モデルのテストを探っていたところ、そもそもMVC構造におけるMの役割にぶち当たりました。
Railsのモデルは一般的にはActiveRecordですが、これはO/Rマッパーで、ドメインモデルとしてビジネスロジックを持たせるかどうかについてはRails界隈で議論になっているようです。
Railsに限らず、MVC的な構造を取るとよく陥るのが肥大化したコントローラーです。
Webアプリケーションではなく、GUIアプリケーションや、ヘッドレスな通信処理アプリケーションなどでも、コントローラーでイベントの受理から応答を返すための集計・加工など(いわゆるビジネスロジック)を手掛けてしまいあっという間に数百〜千行超と肥大化することがあります。
その解決の一方法として、O/Rマッパー部分のActiveRecord継承クラス(物理モデル)と、ビジネスロジックを持つ論理モデルに分割し、コントローラーからは論理モデルにアクセスするという設計があります。
コード上では、
- 論理モデルをクラスとして別定義する。コントローラーからは論理モデルにのみアクセス、物理モデルは論理モデルからのみアクセス可能とする。
- 論理モデルをモジュールとして別定義し、ActiveRecord継承クラスでincludeする。コントローラーからはモジュールに定義したメソッドを呼び出し、ActiveRecord継承クラスのメソッド(属性)には触らない。
の2案があります。後者が推奨される模様です。
ActiveRecord継承クラスのテスト
- validationのテスト
- データベースの整合性テスト(制約、外部参照など)
をテストします。
テストデータは、fixtureで用意すると制約違反なデータは用意できないので、まずfixtureでは制約を守ったデータを用意し、テストメソッドでカラムに制約違反となる値を入れてvalidをassertで確認するよウニテスト記述します。
require File.dirname(__FILE__) + '/../test_helper' class TermCategoryTest < ActiveSupport::TestCase plugin_fixtures :term_categories def setup @category = TermCategory.find(1) end def test_valid assert @category.valid? end def test_invalid_without_name @category.name = nil assert_not @category.valid?, 'saved term_category without a name' assert_not_nil @category.errors[:name], 'no validation error for name present' end
ここで、invalidのテストをするにあたり、前提としてモデルクラスでvalidateされている属性となります。モデルクラスでvalidateしていないでデータベースの制約がかかっている場合、valid?はtrueになりますが、モデルをsaveすると例外が発生します(例、ActiveRecodrd::NotNullViolation)。
validationで必須指定をしていないがデータベース上はNOT_NULL制約をかけている属性(カラム)については、次のようなコードでテストできます。
def test_violation_notnull_author_id @term.author_id = nil assert_raises ActiveRecord::NotNullViolation do @term.save end end
テストのデバッグ
ユニットテストを実行して問題が発生したときに、その場所での変数の値を確認したり、ステップ実行させることができると重宝します。というかそれができない開発環境は貧弱です。
Rubyでは、pry-byebugライブラリを使い、ブレークポイントを置きたいコード上にbinding.pryと追記するとそこでブレークしてコマンド待ち(REPL)となります。
その他
assertの書き方メモ
空文字列の判定
assert_empty @term.value('category')
日時の判定(一致判定)
assert_equal '2010-12-13 21:25:16'.to_datetime, @term.value('datetime')