LoginSignup
0
2

More than 3 years have passed since last update.

新卒1年目がTDDを活かした実装方法を考えさせていただきました(ruby)

Posted at

なぜ新卒1年目がTDDに注目したのか

現在新卒1年目でエンジニアとして働いている。エンジニアリングの経験が浅い自分が、経験の浅さを活かして先輩に何かで勝てないか考えていた。

経験が浅いので実装手順に癖がない。癖がないからこそ、いい癖をつけておこうと考えてTDDを活かした実装に挑戦しようと思った(もっと違うやり方があったかもしれません)。

実際にやったこと

TDDとは

テスト駆動型開発。実装する前に、実装がうまく行った時にのみパスするテストを書くということ。

TDDのイメージ

テストを書く(実装前)

テストが失敗する(何も実装していないから)

実装をする

テストが成功する(実装によって、実装前に書いたテストをパスできるようになったから)

リファクタリングを行う。
これによりテストが成功していれば、機能を失わずにコードを修正できたと言える。
逆にリファクタリングにより、テストが失敗すれば、実装したはずの機能を失ってしまっているので、デバッグをする。

TDDのメリット、デメリット

TDDのメリット

  • 安全なリファクタ
  • 機能追加によるバグが防げる

TDDのデメリット

  • いちいち書くのはめんどくさい
  • どんなテストを書けば良いかわからない
TDDするかの指標

TDDのデメリットを抑えて、メリットを活かすためにはどんな時にTDDするのかの指標が必要。

  • 単純なテスト(例:リクエストが成功しているか)→TDD
  • 動作、開発内容が完璧に決まり切っていない段階→開発が先
  • セキュリティー的に重要な開発やバリデーションを確認したい時→TDD
  • リファクタリングをしたい→先にテストを書いて、リファクタリングしてもテストがパスするか確認

要するに基本TDDしようという姿勢で良いが、まずテストどう書いたら良いかわからないと感じた時はTDDするべきではない。

TDDする時のテストの具体例

ここからTDDをしたテストの具体例をあげる。様々なテストの種類(model,controller,integrationなど)を用いた説明になるので、テストの種類について知らない方はこちらを見ていただければと思います。
railsのテストディレクトリ構造とテスト処理

単純なテスト

リクエストした時に成功するか、必要な要素の存在確認(単純なcontroller test)。シンプルな確認テストは先に書くと良いということ

controller_test.rb
#コントローラーテスト
  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)

  • モデルクラスに対してバリデーションの確認をする時
user_test.rb
#ユーザーモデルのテスト
#ユーザーモデルでバリデーション内容の記述をする前に、以下のテストを書く
  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を使った送信(新規登録、ログインなど)に対してのバリデーションを確認する時
users_signup_test.rb
#新規登録のテスト(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(二つの目立たないバグ)を参考にいたしました。

Login_controller.rb
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をする。ログアウト二回行った時に正常な動作になっているか確認するテストを先に書くのである。

users_login_test.rb
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

このテストをパスするために、機能に変更を加える。最終的に以下のようなコードを追加するとテストがパスする。

Login_controller.rb
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に限らず、バリデーションのテストを記述する時は、うまく行って欲しい時のテストとうまく行ってほしくない時のテストの両方書かなければいけない。

users_signup_test.rb
#新規登録のテストを行う
#無効になるパラメータで登録しようとして失敗して欲しいテスト
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を書いて、テスト実施されているか確認するとよい。
controller
  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

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2