渡り鳥の旅路

元半導体系エンジニア、今Webエンジニアの雑記

【RailsでTDDを学ぶ】Rails 5 Test Prescriptionsの紹介

はじめに

7月から個人的にテスト強化月間をしているのですが、今読み進めているRails 5 Test PrescriptionsがRSpecでテスト駆動開発を学ぶ上で良い本なので紹介です。

そもそもなぜテスト強化をテーマにしているかというと、自社のメンターに「どこまでできたらプログラマーとして一人前なのか?」と聞いたことをきっかけとします。

「リファクタリングが身についたら」

答えはその一言でした。

ただ言葉としては非常にシンプルなのですが、この状況を作るにはいくつか条件が必要です。自分なりの答えはこんな感じです。

  • 扱う言語/フレームワークに習熟している
  • 設計の知識がある
  • 適切なテストを書いている
  • リファクタリングする時間を捻出できる(ある程度の実装スピードがある)

そもそもある程度の規模のプロダクトになると、リファクタリングする前提としてテストが必要なのです。

しかしながらその頃はテストに対してつらみしか感じておらず、義務駆動で書いている状況でつらいつらい、もっと楽しくテストを利用できないのかなぁと沈んだ気持ちでテストと向き合っていた頃にタイミングよく、Tama.rb会議で伊藤淳一さんがテストに関する発表をされることに!

blog.jnito.com

そのなかでで挙げていたテストの役割の6つ目、

設計を支援する

をみて「もしかしてこの考え方なら前向きにテストに取り組めるかも」とピピンッときて、これまた処方箋の6つ目

ちゃんと、もがき苦しんでますか?試行錯誤しまくってますか?

で「やっぱり強い人ももがいてきたからこそ今があるんだよな」となり、テスト熱が上がって動き出したのでした。

本の紹介

pragprog.com

前置きが長くなりましたが、本の紹介です。

RSpecの入門書であるEveryday Railsのあとがきで紹介されていてこの本を知りました (紹介文ではRails4版でしたが、Rails5版が出ています)。

実はAmazon kindleでも購入できるのですがThe Progmatic Bookshelfだと半額相当(〜2700円)で購入できるのでこちらがオススメです。

対象読者としては

  • Ruby/Railsの中級者以上
  • テストに関しては初心者でOK
  • テスト駆動開発(TDD)を身に着けたい人

になります。

この本では、最初にテストを書かない開発スタイルと、テスト書く開発スタイルを並べて、 時系列的にどのように開発コストが変わっていくかを、読者に想起させるところから始めます。

それからハードルの低いTDDの始め方を紹介し、各トピックに対して手法を比較しケースごとに向いている方法を実践していきます。一面的な見方ではなく、手法のメリット・デメリットに言及することで、読者が実践に適した手段を選べるように配慮されています。

目次

0. Preface
1. A Test-Driven Fable
2. Test-Driven Development Basics
3. Test-Driven rails
4. What Makes Great Tests
5. Testing Models
6. Adding Data to Tests
7. Using Test Doubles as Mocks and Stubs
8. Integration Testing with Capybara and Cucumber
9. Testing JavaScript: Integration Testing
10. Unit-Testing JavaScript
11. Testing Rails Display Elements
12. Minitest
13. Testing for Security
14. Testing External Services
15. Troubleshooting and Debugging
16. Running Tests Faster and Running Faster Tests
17. Testing Legacy Code

メモ(まだ途中なので徐々に増えます)

2. Test-Driven Development Basics

Where to Start

  • specify the initialization state of the objects or methods under test
  • happy path: a single representative example of the error--free version of the algorithm

どうするか迷ったら、 筆者はinitial state→happy pathの順をススめる。

The general form of an RSpec expectation is expect(actual_value).to(mather).

RSpecのBuilt in matcher https://relishapp.com/rspec/rspec-expectations/v/3-7/docs/built-in-matchers

The Second Test

所見だと戸惑いそうなやつ

One of my other favorite bits of RSpec is an implicit matcher 
that RSpec creates by name-mangling if you give it a matcher it doesn’t recognize. 

例えば Task.done?メソッドがあったとして、マッチャーとしてbe_doneを使える(RSpecが勝手に推測してくれる)。

3. Test-Driven rails

Let's Write some Rails

一度外側からアプリケーションをテストすることのメリット

There are several reasons why it’s valuable to have a test like this one that works from outside the application: 
  • ソースコードの内部構造を推定しなくてよい
  • ユーザー/クライアントから見えるfeatureについて考えることを強いてくれる。
  • 一連の動作をテストすることで、ユニットテストのギャップに潜むバグを抑制することができる。

A test with a view

はじめにゆるいviewのテストを書いて、あとでリファクタしながらタイトなテストにしていく。

This is a really common work pattern for me. 
Sometimes I have trouble seeing the shape of a view before I write it, 
so I write a very loose test and then tighten the test once I see 
what pieces of the view will exist for me to hook onto. 

5. Testing Models

A TDD Metaprocess

テストを追加する順番

  1. 初期状態
  2. シンプルな正常系
  3. 他の正常系
  4. 異常系、エッジケース

image.png (94.8 kB)

テストの重複

  • 事実の重複
  • ロジックの重複
  • 構造の重複

6. Adding Data to Tests

データを用意するための2つのテクニック

  • Fixtureを利用する
  • Factory toolを利用する

(テストダブルのチャプターでは完全に違う考え方をすることになるのだが)

全ての状況に対して完璧に答えてくれる方法はない。

どんなときにつらくなるか、想像してみる

Userに対するテスト 最初は

user = User.create!(date_of_birth: '2000-01-01')

で済んでいたのに、認証機能が入り、

user = User.create!(date_of_birth: '2000-01-01', emil: 'sample@gmail.com', password: 'password')

追加で取得する個人情報が増え、

user = User.create!(date_of_birth: '2000-01-01', emil: 'sample@gmail.com', password: 'password', hight: 170, zip_code: '9870012')

とどんどんデータの準備が増えていく、、

そのたびに壊れるテスト、、

Fixture

yamlを管理するのは大変だが、データを用意する速く簡単な方法には違いない。 テスト実行前に一気にデータを作成してくれる。いわゆるbefore(:suite)と同じ?

しかし、 例えばfixtureで一つのダメデータを作ると、テストが壊れてしまう。。

→Fixturesはglobal semantic dataがデータベースに必要なときに効力を発揮する。

Factory

Basic factory Creation

  • build(:project)
  • create(:project
  • attributes_for(:project):factoryの全てのattributesをハッシュで返す。コントローラテストでparamsを扱うときに便利。
  • build_stubbed(:project):この本で初めて知ったが便利そう

7. Using test Doubles as Mocks and Stubs

この章に書いてあることを最初から全て理解しようとするのはtoo muchな感じ

普段の開発では伊藤さんの記事を理解していれば十分では https://qiita.com/jnchito/items/640f17e124ab263a54dd

  • スタブとは? A stub is a fake object that returns a predetermined value for a method call without calling the actural method on an actual object.
# スタブはdoubleメソッドで作れるが、下記は特定のメソッドのみに適用するパーシャルスタブ
allow(thing).to receive(:name).and_return("Fred")
  • モックとは? A mock is similar to a stub, but in addition to returning the fake value, a mock object sets a testable expectation that the method being replaced will be called in the test. If the method is not called, the mock object triggers a test failure. expect(thing).to receive(:name).and_return("Fred")
# allowではなくexpectを使う
expect(thing).to receive(:name).and_return("Fred")

Mock Tips

  • Don't mock what you don't own

    • 自分のアプリが持つメソッドにのみテストダブルを適用し、(アプリからみて)外部メソッドに対しては適用しないこと(例:ActiveRecordのupdate_attributesメソッド)
    • そういうことをしそうになったら、自分のアプリ側にサードパーティのフレームワークを呼び出すメソッドを用意し、そのメソッドに対するスタブを作ること。
  • When to Mock, When to Stub

    • スタブを使うケース
      • テスト環境で作成が難しい/不可能なリアルなオブジェクトを用意したい時
    • モックを使うケース
      • 自分のコードの違うシステム間の連携をテストしたい時
      • モックはサブシステム間の境界をテストするのに向いている
      • 副作用を持っていたり、他のメソッドを呼ぶメソッドに対してモックを作る時は注意
      • モックはオリジナルのメソッドをバイパスする(=副作用や内部メソッドの呼び出しは再現しない)
  • Mocks Are Design Canaries

    • 一つのテストの中で多くのモックを使っている時は過分なテストをしているか、オブジェクト指向的に良くない設計になっていることを示唆している。

業務に取り入れてみて

この本はまだ読み途中なのですが、並行して業務でTDDを導入しています。

最初は1つ目のテストを通すまでに時間がかかり、実装までの初速が遅いなぁという感覚が強かったのですが、一度ベースとなるテストと実装ができるとそこからがはやいです。リファクタや機能の修正と併せてテストを回すことで、自分の変更に自信を持って開発を進めることができます。

Kent Beckの[テスト行動開発 付録C]に

テスト駆動開発は、実際に手を動かしてみないと理解が難しい技法です。
本書も、読んだだけでは深い得心には至らないでしょう。
しかし、テスト駆動開発の良さ、強みは手を動かせばわかります。
なぜなら、TDDの本質は精神状態のコントロール、不安と自信の制御にあるからです。
結果(書かれたコードとテストコード)ではなく、
過程(思考プロセスとリファクタリング)に本質があります。

と書いてあり、まさにその通りだなと思えるようになりつつあります。

まとめ

テスト関してまだまだ修練あるのみですが、実践することでリファクタしやすい状況ができるなと実感できました。よりスムーズに進められるよう色々実験していきます。