前置き
- こちらの記事はソニックガーデン 若手プログラマ Advent Calendar 2024の3日目の記事です。
- ソニックガーデンは「納品のない受託開発」という開発手法を取っており、お客様への「納品」がありません。「納品」がない代わりに、お客様と1つのチームとして一緒に作り続けていきます。
- スピード感を持って開発を進めていくために「テスト」というものがどのような役割を持っているのか、「テスト」を書くと何が良いのかを語っていこうと思います。
そもそも何のためにテストを書くのか
- 納品がある受託開発の場合、テストを書く理由は検品のためでしょうか?
- 前職の受託開発の経験が少なく、疑問形なのはお許しください
- ソニックガーデンでは納品だけでなくドキュメントも基本はありません。そのかわりにテストを仕様書とみなして開発を進めています。
- テストがあることで、スピード感を持って品質高く開発を進めることが可能になります。
テストに対する苦手意識について
- 今でこそ、テストが大事だと思っていますが、入社当時は苦手意識がありました。実際なかなかテストを書くまで回らない時期もありました。
- 具体的な苦手部分は以下のような部分です。
テストコードを書くのが難しい
- 「テストが難しい」ではなく「テストコードを書くのが難しい」です。
- ソニックガーデンでは Ruby on Rails を使って開発を行い、テストフレームワークとしてRSpecがよく使用されています。
- RSpecに限らず、テストコードはバックエンドやフロントエンドのコードとは別の頭を使って書く必要があります。
- つまり、書き方やテストの観点がわかっていないとなかなか手が出しづらいものなんですね。
- 結局、手が出しづらいから「書く必要があるから書く」という状況になり「でも難しいから書けない」という悪循環に陥っていました。
テストコードを書く時間(コスト)がかかる
- あるあるですね。
- 機能そのものの実装だけでなく、それが
期待通りに動いているということを確かめるだけ
のためにまたコードを書かなくてはならない。と考えたらたしかにコストが掛かるのも頷けます。 - 実際、実装は数行で済むのにテストを書くほうが時間がかかるということもよく起こります。テスト書かなければ新機能もっと作れるのになぁ・・・となることもしばしば
テストに対する認識を改めた
- そんな自分でしたが、あるときふと「あれ?これテストある方が結果的に早いんじゃないか?」と思う時期が来ました。それはきっと若手ならではだったのかもしれません。
入社直後
- 入社したての自分はまず「動くことが正義」でした。なので、「今」動かす(動いている)ことこそが全てでそのためにコードを書いていました。 仕事量もそこまで多くなく、書いた部分を手動でテストすれば十分済むくらいのレベルでした。
- 時間が経つにつれ、毎週のタスクもだんだん多くなり、設計したり考えたりする時間が増えました。今までみたいに今あるタスクを見て動いていたら時間が足りなくなってきたのです。
- そこで、特に影響が出てきたのが「コードレビューの対応」と「動作確認」です。
- 徒弟制度の弟子の場合、リリースされるコードは親方にコードレビューをもらいます。若手の場合、そもそも変な設計をしていたり不合理なコードを書いたりと指摘まみれになります。
- メソッドの命名
- カラム名
- ロジック
- 全体を考えたときの設計
- そもそもやりたいことからズレている → 一番痛い・・・
- etc
- 指摘をもらって直せば直すほど、自分の血肉になるのはわかっているのですが、対応することそのものにも時間はかかります。
- 当然、指摘をもらった「コード」を直した後「動作確認」を行うのですが、ここに時間がかかっていたことに気がつけていませんでした。
- 直したんだから動作確認するのは当たり前で、そこに時間がかかるのも当たり前だと思っていました。
- しかも、1項目直したら手動でテストしていたので、直した項目の周辺コードに別の修正があればコードを修正したことでバグっていないかを確かめるのに何度も何度も同じ手動テストを繰り返していました。
入社から1年ちょっとくらいした後
- いつも通りひーひーいいながらコードレビューの対応をしていたとき、「あれ?これ自動テスト書いてあれば一瞬で済んだのでは?」と気が付きました。
- そうです。自動テストが書いてあれば「毎回は手を動かさなくても良い」ということです。
- 同じ機能の中で指摘事項を修正した後自動テストが通ったことを確認して、最後に画面を1度触って動作確認完了です。
- このことに気がついて以来、以前よりかなり頻繁にテストを書くようになったため、テストを書く時間を確保したり、テストの書き方を覚えてきました。
- 結果、今までより自信を持ってリリースに望めたり、(時間的に)気軽にコードレビュー対応を行えたりと良いことずくめになりました。
終わりに
- テストを書くことは強制力が働くものではなく「文化」です。
- 周りにテストを書く人がいなければなぜテストを書くのか、なぜ書くとよいのかに気が付けないでしょう。
- 特に若手の方で、コードレビューを受ける機会が多い方はテストと一緒にコードレビューを受けることで、その指摘をすぐに直して開発速度UP & 品質UPを狙いましょう!
(オマケ)具体的に書くテストとその観点について
- ここからはオマケです。
- やはりプログラマーなので具体的になコードがあるほうが良いかなと思ったので、ここからはどんな観点でどんなコードを書いているのかを書いておきます。
モデルケース
- 商品を販売するECサイトを作成します。
- 管理者は商品を一覧、登録、編集、削除することが出来ます。(CRUD)
ER
- users
- 省略
- admins
- 省略
- products
- name:string
- description:text
- price:integer
routes
namespace :admis do
resources :products, only: %i[index show new create update destroy]
end
テストコード
# admins/products_spec.rb
describe '商品' do
let(:admin) { create(:admin) }
before { sing_in admin }
describe '一覧画面' do
it '商品登録画面へのリンクがあること' do
visit admins_products_path
expect(page).to have_link '新規登録', href: new_admins_product_path
end
it '商品編集画面へのリンクがあること' do
product = create(:product)
visit admins_products_path
expect(page).to have_link '編集', href: edit_admins_product_path(product)
end
it '商品が一覧できること' do
create(:product, name: 'トマト', price: 100)
visit admins_products_path
expect(page).to have_content 'トマト'
expect(page).to have_content '100円'
end
end
describe '詳細画面' do
it '商品の情報が表示されていること' do
create(:product, name: 'トマト', description: 'まるでフルーツみたいに甘いトマトです', price: 100)
visit admins_products_path
expect(page).to have_content 'トマト'
expect(page).to have_content 'まるでフルーツみたいに甘いトマトです'
expect(page).to have_content '100円'
end
end
describe '登録画面' do
it '登録が出来ること' do
visit new_admins_product_path
fill_in '商品名', with: 'りんご'
fill_in '商品説明', with: '採れたてのりんごを直送します!'
fill_in '価格', with: 200
expect do
click_button '登録する'
expect(page).to have_content '登録しました'
end.to change { Product.count }.from(0).to(1)
product = Product.order(:id).last!
product_attributes = {
name: 'りんご',
description: '採れたてのりんごを直送します!',
price: 200,
published_at: nil,
}
expect(product).to have_attributes(product_attributes)
end
end
describe '編集画面' do
it '編集が出来ること' do
product = create(:product, name: 'みかん', description: '甘くて美味しいです。', price: 300, published_at: nil)
visit edit_admins_product_path(product)
fill_in '商品名', with: 'バナナ'
fill_in '商品説明', with: '色味が良く、贈り物におすすめ!'
fill_in '価格', with: 400
expect do
click_button '更新する'
expect(page).to have_content '更新しました'
end.to change { product.reload.name }
.from('みかん').to('バナナ')
.and change { product.reload.description }
.from('甘くて美味しいです。').to('色味が良く、贈り物におすすめ!')
.and change { product.reload.price }
.from(300).to(400)
end
it '削除が出来ること' do
product = create(:product)
visit edit_admins_product_path(product)
expect do
accept_alert { click_button '削除する' }
expect(page).to have_content '削除しました'
end.to change { Product.count }.from(1).to(0)
end
end
end
考えているテストの観点
- その画面の仕様をテストできているか
- 例えば一覧画面のテストの場合、「新規登録画面へ遷移できたり編集画面への遷移が出来ること」はその画面の仕様です。
- そのため、新規登録画面や編集画面への遷移の「リンクがあること」を検証しています。
- テストしようと思えば、新規登録画面に遷移して登録まで行うことまで検証できますが、観点としてはあくまで「遷移が出来ること」と考えているのでリンクがあることの検証に留めています。
- DBに期待した値が保存されているか
- 例えば登録や編集のテストの場合、「DBに期待した値が保存されているか」を確認するようにしています。
- 人によっては「画面に表示されていること」を検証する方もいますが、自分はあくまで本質はDBに保存されることだと思っています。
- 画面に表示されることが大切なのであればそれは別でテストが存在するべきだ、という主張です。
- あまりDRYにしすぎない
- 今回のテストではproduct を let で定義して使い回すことも出来ます
- しかし、このテストの中では要素の検証で使っているので it の中で1つ1つ定義するようにしています。
- もし let で定義したとき、特定の値のときだけ検証したい みたいな項目を満たすのが難しくなります。
- もちろん、そうすることで得られるメリットやそうすべきタイミングもあるかもしれないですが、迷うならまずは1つ1つ定義しておいてよいかなと考えています。