#なぜ新卒1年目がTDDに注目したのか
現在新卒1年目でエンジニアとして働いている。エンジニアリングの経験が浅い自分が、経験の浅さを活かして先輩に何かで勝てないか考えていた。
経験が浅いので実装手順に癖がない。癖がないからこそ、いい癖をつけておこうと考えてTDDを活かした実装に挑戦しようと思った(もっと違うやり方があったかもしれません)。
###実際にやったこと
-
Ruby on Rails チュートリアルでTDD開発をしてみる。
sample_app -
テスト駆動開発をざっと読んでみる
-
テスト駆動開発の著者の動画を見てみる。
Clean code that works - How can we go there? - Takuto Wada | SeleniumConf Tokyo -
社内のTDDの勉強会の資料を閲覧
#TDDとは
テスト駆動型開発。実装する前に、実装がうまく行った時にのみパスするテストを書くということ。
#####TDDのイメージ
テストを書く(実装前)
↓
テストが失敗する(何も実装していないから)
↓
実装をする
↓
テストが成功する(実装によって、実装前に書いたテストをパスできるようになったから)
↓
リファクタリングを行う。
これによりテストが成功していれば、機能を失わずにコードを修正できたと言える。
逆にリファクタリングにより、テストが失敗すれば、実装したはずの機能を失ってしまっているので、デバッグをする。
#####TDDのメリット、デメリット
TDDのメリット
- 安全なリファクタ
- 機能追加によるバグが防げる
TDDのデメリット
- いちいち書くのはめんどくさい
- どんなテストを書けば良いかわからない
#####TDDするかの指標
TDDのデメリットを抑えて、メリットを活かすためにはどんな時にTDDするのかの指標が必要。
- 単純なテスト(例:リクエストが成功しているか)→TDD
- 動作、開発内容が完璧に決まり切っていない段階→開発が先
- セキュリティー的に重要な開発やバリデーションを確認したい時→TDD
- リファクタリングをしたい→先にテストを書いて、リファクタリングしてもテストがパスするか確認
要するに基本TDDしようという姿勢で良いが、まずテストどう書いたら良いかわからないと感じた時はTDDするべきではない。
#TDDする時のテストの具体例
ここからTDDをしたテストの具体例をあげる。様々なテストの種類(model,controller,integrationなど)を用いた説明になるので、テストの種類について知らない方はこちらを見ていただければと思います。
railsのテストディレクトリ構造とテスト処理
###単純なテスト
リクエストした時に成功するか、必要な要素の存在確認(単純なcontroller test)。シンプルな確認テストは先に書くと良いということ
#コントローラーテスト
test "should get home" do
get root_path
assert_response :success is #リクエストが成功するか
assert_select "title", "HOME TITLE" #ページのタイトルがHOME TITLEになっているか
end
###バリデーション
バリデーションがうまく行っていれば成功するテストを、最初に書く。(例: model test, form送信の結果を確認するintegration test)
- モデルクラスに対してバリデーションの確認をする時
#ユーザーモデルのテスト
#ユーザーモデルでバリデーション内容の記述をする前に、以下のテストを書く
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "hogehoge", password_confirmation: "hogehoge")
end
test "should be valid" do
assert @user.valid?
end
test "name should be present" do
@user.name = " "
assert_not @user.valid?
end
test "email should be present" do
@user.email = " "
assert_not @user.valid?
end
- formを使った送信(新規登録、ログインなど)に対してのバリデーションを確認する時
#新規登録のテスト(integration_test)を行う
#無効になるパラメータで登録しようとして失敗して欲しいテスト
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
end
end
###機能修正(デバッグ)のためのTDD
特にエラー文が出ているわけではないが、自分の思った通りに動いていない時に先にテストを記述。
自分の思った通りに動いた時にのみパスするテストを先に書き、その後自分の思った通りに動くようにコードに変更を加える。
ログアウトの処理を例に挙げて説明する。Ruby on Rails チュートリアル 9.14(二つの目立たないバグ)を参考にいたしました。
class LoginController < ApplicationController
.
.
def destroy #ログアウトするためのアクション
log_out
redirect_to root_url
end
private #これ以降はアクション内で使用する関数の定義
# 永続的セッションを破棄する
def forget(user)
user.forget #DBに保存されているuserのログイン記憶トークンを空にする
cookies.delete(:user_id) #cookiesの中身を空にする
cookies.delete(:remember_token)
end
# 現在のユーザーをログアウトする
#現在のユーザを示すcurrent_userの記憶トークンを空にし、session,変数current_userの中身も空にする
def log_out
forget(current_user)
session.delete(:user_id)
current_user = nil
end
end
このコードの状態で、
ブラウザで2つのタブを開き、それぞれのタブで
同じユーザーでログインしている状況を想定する。
まず一つのタブでログアウトする。するとcurrent_userの値ががnilになる。
その後にもう一方のタブでログアウトしようとするとcurrent_userがnilなのでnil.forgetになり、エラーになってしまう。
このような状況は、「特にエラー文が出ているわけではないが、自分の思った通りに動いていない時」と言えるので、TDDをする。ログアウト二回行った時に正常な動作になっているか確認するテストを先に書くのである。
def setup #loginするユーザーインスタンスを定義
@user = User.new(name: "Example User", email: "user@example.com",
password: "hogehoge", password_confirmation: "hogehoge")
end
test "two times logout after login" do
log_in(@user)
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
#別のタブでログアウトしたことをシュミレート
delete logout_path
follow_redirect! #リダイレクトすると、ログイン前のページになっているか確認
assert_select "a[href=?]", login_path #loginページのためのパスがあるか確認
assert_select "a[href=?]", logout_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0
end
このテストをパスするために、機能に変更を加える。最終的に以下のようなコードを追加するとテストがパスする。
class LoginController < ApplicationController
.
.
def destroy #ログアウトするためのアクション
log_out if logged_in? #login状態のときだけlogout関数使用
redirect_to root_url
end
end
#コツ・その他
テスト駆動開発という本からTDDを実装する時に使用するコツを学んだ。
この本には最低限テスト作成→仮実装→三角測量→リファクタリングをしようと書いてあった。
今回は入力された引数(int)を文字列にして返す関数を、作成するためのTDDを例にあげる。
###最低限テスト作成
関数(まだ作成されていない)のテストを作成する。作成した時点ではテストがredになる。
test "Pattern1 of test returnString function " do
a = returnString(1)
assert_equal a, "1"
end
###仮実装
どんな形であれ、上で書いたテストが通るような関数を定義する。関数定義でテストがgreenになる。
def returnString(int)
return "1"
end
###三角測量
同種のテストを複数作成(今回は2個)し、そレラのテストがパスするように実装を一般的な形に書き換える
- まず似たようなテストをもう一つ追加
このテストを作成するとredになってしまう。なぜなら今の実装では"1"しか返さないから。
test "Pattern2 of test returnString function " do
b = returnString(2)
assert_equal b, "2"
end
- 実装を一般的な形に変更する
1という生の値を返す関数から、引数で受け取った値を文字列にして返す関数に変更する。テストはgreenになる。
def returnString(int)
return "#{int}"
end
###リファクタリング
上記のように最低限のな実装をするためにTDDをしても、さらに追加したい機能が増えて、コードは複雑になっていく。
でも大丈夫。テストを書いているおかげでコードの機能が正確は判断してもらえる。よって安心してリファクタリングができる。
#TDDを実践してみて感じたこと
-
TDDをすることで自分がまず何を実装したいのか考えれる。それが実装を効率的にしていると感じた。またテストを書いておくことで安心してリファクタリングができるのでコードの保守性を維持しやすくなるだろうなと感じた。
-
TDDに限らず、テストを記述する時は複種のテスト内容を一気に記述しようとしなくて良い。一気に書こうとしてtest内容自体が間違ってしまっては本末転倒
-
上の内容とかぶるがテストの内容を網羅的に書きすぎることはよくない。網羅的に書きすぎる(HTML要素の存在を徹底的に確認等)と、今後機能に変更があった場合テスト自体も保守、修正する手間が増えるから。
-
TDDに限らず、バリデーションのテストを記述する時は、うまく行って欲しい時のテストとうまく行ってほしくない時のテストの両方書かなければいけない。
#新規登録のテストを行う
#無効になるパラメータで登録しようとして失敗して欲しいテスト
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
end
end
#有効になるパラメータで登録しようとして成功して欲しいテスト
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
end
- raiseを記述することで、その場面で意図的にエラーを発生させる。raiseを記述したのにも関わらず、テストが全て通るということは、raiseを記述した部分がテストされていないことになる。条件分岐(if文)の中でraiseを記述して、その分岐がテストされているか確認する。テストされているか怪しい部分にraiseを書いて、テスト実施されているか確認するとよい。
def create
if (user_id = session[:user_id])
.
.
else
raise # テストがパスすれば、この部分がテストされていないことがわかる→この部分がテストされるようにテストを書く
.
.
end
end
#参考文献
Ruby on Rails チュートリアル
テスト駆動開発
Clean code that works - How can we go there? - Takuto Wada | SeleniumConf Tokyo