Upgrade to Pro — share decks privately, control downloads, hide ads and more …

とりあえず抑えておきたい Railsでの「テストの内容」の考えかた #kaigionrails...

ShinkuFencer
October 22, 2022

とりあえず抑えておきたい Railsでの「テストの内容」の考えかた #kaigionrails /Test code concepts to keep in mind for now with Rails

Kaigi on Rails 2022 Day2 で発表する内容です。

【資料内参考したものリンク】

texta.fm - 9. The 20th Anniversary of TDD​​
https://texta.pixta.jp/entry/2022/03/10/150000

ASTERセミナー標準テキスト|Kouichi Akiyama|note
https://note.com/akiyama924/m/m6981810393cd

Software Design 2022年3月号|技術評論社
https://gihyo.jp/magazine/SD/archive/2022/202203

ソフトウェアテスト技法練習帳 ~知識を経験に変える40問~
https://gihyo.jp/book/2020/978-4-297-11061-1

ShinkuFencer

October 22, 2022
Tweet

More Decks by ShinkuFencer

Other Decks in Technology

Transcript

  1. リリース 手動動作確認 Viewの修正 実装直後の動作確認 11 Controllerの修正 Modelの修正 仕様に沿って Userモデルのnameに Validationを入れよう

    開発実装 担当 テストコードを書かない場合… validates :name, length: { minimum: 5 } ▼仕様 User.nameのバリデーション 名前が5文字以下はエラー。 ・「めでぃかる太郎」はOK ・「のーと次郎」はエラー
  2. リリース 手動動作確認 Viewの修正 実装直後の動作確認 14 Controllerの修正 Modelの修正 開発実装 担当 あれ?5文字で入れたけど

    エラーが何も出ない? 確か仕様では5文字以下で エラーしたよね? 手動テスト 担当 ! テストコードを書かない場合… あれ?なんでだろう? もしかしてControllerに 正しく値が渡ってきてない?
  3. リリース 手動動作確認 Viewの修正 実装直後の動作確認 15 Controllerの修正 Modelの修正 開発実装 担当 手動テスト

    担当 ! 初手の実装誤りを一通り作った後に気づき手戻りが発生! テストコードを書かない場合… しまった! Validateのminimumが5だと 5文字のときはsaveできる! 「:minimum: 属性はこの値より小さな値を取れません。」なので 指定した値自体はvalidationのerrorにならない https://railsguides.jp/active_record_validations.html#length
  4. リリース 手動動作確認 Viewの修正 実装直後の動作確認 16 Controllerの修正 Modelの修正 実装タイミングでテストコードを書くと… 仕様に沿って Userモデルのnameに

    Validationを入れよう 開発実装 担当 validates :name, length: { minimum: 5 } ▼仕様 User.nameのバリデーション 名前が5文字以下はエラー。 ・「めでぃかる太郎」はOK ・「のーと次郎」はエラー
  5. リリース 手動動作確認 Viewの修正 実装直後の動作確認 17 Controllerの修正 Modelの修正 実装タイミングでテストコードを書くと… 仕様に沿ってテストコードを書こう 開発実装

    担当 ▼仕様 User.nameのバリデーション 名前が5文字以下はエラー。 ・「めでぃかる太郎」はOK ・「のーと次郎」はエラー 仕様の記述にならって 『nameが「めでぃかる太郎」だと保存できる』 『nameが「のーと次郎」だと保存に失敗する』 というテストコードにしよう
  6. リリース 手動動作確認 Viewの修正 実装直後の動作確認 19 Controllerの修正 Modelの修正 開発実装 担当 自動テスト

    ? 実装タイミングでテストコードを書くと… あれ、テストコード間違えちゃったかな…? 『nameが「のーと次郎」だと保存に失敗する』 というテストコードが失敗しました
  7. リリース 手動動作確認 Viewの修正 実装直後の動作確認 20 Controllerの修正 Modelの修正 開発実装 担当 自動テスト

    ! あれ、テストケース間違えちゃったかな…? 実装タイミングでテストコードを書くと… しまった!Validateのminimumは 『この値より小さな値を取れません』だから 5を指定したら5文字のときはsaveできる! 早いタイミングで気付ける! 『nameが「のーと次郎」だと保存に失敗する』 というテストコードが失敗しました
  8. 素早く回帰(リグレッション)テストができる 23 ▼仕様 User.profile_textのバリデーション ・20文字以下はエラー ・数字だけで構成されるとエラー ・最初の文字に数字を使うとエラー ・最後の文字に数字を使うとエラー ・「バカ」が含まれるとエラー ・空白文字だけだとエラー

    例) 既にあるユーザのプロフィール文章のバリデーションに  『「nil」という文字列が含まれるとエラー』を追加する 手動テスト 担当 新しいバリデーションが 追加されたのか… ここって誰かが前にもテストし た気がしたけど、同じことでき るかな… 手動だと過去と同じ動きをすることができるとは限らない 作業量がふえるとヒューマンエラーで取りこぼす可能性もある 量が多いからテストの抜けもれが ないようにしないと…
  9. 素早く回帰(リグレッション)テストができる 24 ▼仕様 User.profile_textのバリデーション ・20文字以下はエラー ・数字だけで構成されるとエラー ・最初の文字に数字を使うとエラー ・最後の文字に数字を使うとエラー ・「バカ」が含まれるとエラー ・空白文字だけだとエラー

    例) 既にあるユーザのプロフィール文章のバリデーションに  『「nil」という文字列が含まれるとエラー』を追加する 決められたことをテストコード を通してチェックするだけなら いつでもできます 自動テストであれば物量が増えても素早く確認ができる 機械にチェックをお願いするので抜け漏れがない コードで書かれているので 抜け漏れをすることはありません 自動テスト
  10. テスト技法を考える上でのお題 30 お題: 年齢を扱うクラスに文字列を生成するメソッド「output_view_text」を作成したい。 生成する文字列は以下の通り • 0歳から18歳までは「幼年」 • 19歳から30歳までは「青年」 •

    31歳から130歳までは「壮・老年」 なお、0から130以外のものはエラーとしたい。 なお、メソッドに整数値以外のものが入らないことは事前にチェックされているものとする。
  11. ベースとなるクラス ::Plain::Age 31 module Plain class Age def initialize(age_value:) raise

    ArgumentError, "値が整数ではありません " unless age_value.is_a? Integer @age = age_value end def output_view_text raise StandardError, "値が範囲外です" if @age < 0 return "幼年" if @age <= 18 return "青年" if @age <= 30 return "壮・老年" if @age < 130 raise StandardError, "値が範囲外です" end end end
  12. 同値分割法 - お題をとおして使い方を考える 33 年齢ごとに考えると、人間は0歳から始まる生き物だし 0、1、2、3、4、5、6、7、8、9、10…128、129、130で あとは0ではないときの-1、130を上回る値の131を試してみて 131+2=133パターンの入力値のテストを考えればいいのかな…? お題: 年齢を扱うクラスに文字列を生成するメソッド「output_view_text」を作成したい。

    生成する文字列は以下の通り • 0歳から18歳までは「幼年」 • 19歳から30歳までは「青年」 • 31歳から130歳までは「壮・老年」 なお、0から130以外のものはエラーとしたい。 なお、メソッドに整数値以外のものが入らないことは事前にチェックされているものとする。
  13. 愚直にcontextを書いていく場合 34 context "0歳の場合" do let(:age) { ::Plain::Age.new(age_value: 0) }

    it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "1歳の場合" do let(:age) { ::Plain::Age.new(age_value: 1) } it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "2歳の場合" do let(:age) { ::Plain::Age.new(age_value: 2) } it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "3歳の場合" do let(:age) { ::Plain::Age.new(age_value: 3) } it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "4歳の場合" do let(:age) { ::Plain::Age.new(age_value: 4) } it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "5歳の場合" do let(:age) { ::Plain::Age.new(age_value: 5) } it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "6歳の場合" do let(:age) { ::Plain::Age.new(age_value: 6) } it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "7歳の場合" do let(:age) { ::Plain::Age.new(age_value: 7) } it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end End ## 後略 ### 愚直にやると0から130,-1と131を引数に設定した contextを133個書く必要がある
  14. 同値分割法を利用したテストケース作成 37 • 今回のケースでは有効同値パーティションと無効同値パーティションは 以下の5つとなる 0 -1 131 130 19

    18 31 30 有効同値 パーティション① 「幼年」 有効同値 パーティション② 「青年」 有効同値 パーティション③ 「壮・老年」 無効同値 パーティション① 下限以下エラー 無効同値 パーティション② 上限以上エラー
  15. 同値分割法を利用したテストケース作成 38 • パーティションを表に書き出して、代表値を書き足す。 パーティション名 有効/無効 値の範囲 出力される結果 代表値 幼年

    有効 0以上18以下 幼年 5 青年 有効 19以上30以下 青年 27 壮・老年 有効 31以上130以下 壮・老年 50 下限以下エラー 無効 -1以下 なし(エラー) -3 上限以上エラー 無効 131以上 なし(エラー) 200
  16. 39 context "幼年パターン、 5歳の場合" do let(:age) { ::Plain::Age.new(age_value: 5) }

    it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "青年パターン、 27歳の場合" do let(:age) { ::Plain::Age.new(age_value: 27) } it "青年と出力されること " do expect(age.output_view_text).to eq("青年") end end context "壮・老年パターン、 50歳の場合" do let(:age) { ::Plain::Age.new(age_value: 50) } it "壮・老年と出力されること " do expect(age.output_view_text).to eq("壮・老年") end End context "-3歳の場合" do let(:age) { ::Plain::Age.new(age_value: -3) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end context "200歳の場合" do let(:age) { ::Plain::Age.new(age_value: 200) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end 同値分割法をベースにしたテストケース 各パーティションから任意の値を拾い上げて選択している この場合は 「5」「27」「50」「-3」「200」 となる
  17. おわかりいただけるだろうか… 42 module Plain class Age def initialize(age_value:) raise ArgumentError,

    "値が整数ではありません " unless age_value.is_a? Integer @age = age_value end def output_view_text raise StandardError, "値が範囲外です" if @age < 0 return "幼年" if @age <= 18 return "青年" if @age <= 30 return "壮・老年" if @age < 130 raise StandardError, "値が範囲外です" end end end
  18. 仕様とそぐわない部分 43 module Plain class Age def initialize(age_value:) raise ArgumentError,

    "値が整数ではありません " unless age_value.is_a? Integer @age = age_value end def output_view_text raise StandardError, "値が範囲外です" if @age < 0 return "幼年" if @age <= 18 return "青年" if @age <= 30 return "壮・老年" if @age < 130 raise StandardError, "値が範囲外です" end end end この書き方だと「年齢が130」だと文字列を返さず StandardErrorを返却してしまう 仕様の意図に合わない! ※ただしくは @age <= 130にしなければならない
  19. 44 context "幼年パターン、 5歳の場合" do let(:age) { ::Plain::Age.new(age_value: 5) }

    it "幼年と出力されること " do expect(age.output_view_text).to eq("幼年") end end context "青年パターン、 27歳の場合" do let(:age) { ::Plain::Age.new(age_value: 27) } it "青年と出力されること " do expect(age.output_view_text).to eq("青年") end end context "壮・老年パターン、 50歳の場合" do let(:age) { ::Plain::Age.new(age_value: 50) } it "壮・老年と出力されること " do expect(age.output_view_text).to eq("壮・老年") end End context "-3歳の場合" do let(:age) { ::Plain::Age.new(age_value: -3) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end context "200歳の場合" do let(:age) { ::Plain::Age.new(age_value: 200) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end テストケースも見返す このテストコードでは壮・老年パターンのテストケースは 50で設定しているため気づくことができない
  20. 同値分割法+境界値分析を利用したテストケース作成 48 パーティション名 有効/無効 値の範囲 出力される結果 代表値 境界値 幼年 有効

    0以上18以下 幼年 5 0,18 青年 有効 19以上30以下 青年 27 19,30 壮・老年 有効 31以上130以下 壮・老年 50 31,130 下限以下エラー 無効 -1以下 なし(エラー) -3 -1 上限以上エラー 無効 131以上 なし(エラー) 200 131
  21. 同値分割法+境界値分析を利用したテストケース作成 49 パーティション名 有効/無効 値の範囲 出力される結果 代表値 境界値 幼年 有効

    0以上18以下 幼年 5 0,18 青年 有効 19以上30以下 青年 27 19,30 壮・老年 有効 31以上130以下 壮・老年 50 31,130 下限以下エラー 無効 -1以下 なし(エラー) -3 -1 上限以上エラー 無効 130以上 なし(エラー) 200 131 代表値はケースに合わせてパーティション内の任意の値をとり 境界値はパーティションの範囲の上限と下限を鑑みて設定 無効パーティションの場合は上限や下限が両方あるとは限らな いので内容に応じて追加する
  22. 50 context "青年パターン、 30歳の場合" do let(:age) { ::Plain::Age.new(age_value: 30) }

    it "幼年と出力されること " do expect(age.output_view_text).to eq("青年") end end context "壮・老年パターン、 31歳の場合" do let(:age) { ::Plain::Age.new(age_value: 31) } it "壮・老年 と出力されること " do expect(age.output_view_text).to eq("壮・老年") end end context "壮・老年パターン、 50歳の場合" do let(:age) { ::Plain::Age.new(age_value: 50) } it "壮・老年 と出力されること " do expect(age.output_view_text).to eq("壮・老年") end end context "壮・老年パターン、 130歳の場合" do let(:age) { ::Plain::Age.new(age_value: 130) } it "壮・老年 と出力されること " do expect(age.output_view_text).to eq("壮・老年") end end context "131歳の場合" do let(:age) { ::Plain::Age.new(age_value: 131) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end context "200歳の場合" do let(:age) { ::Plain::Age.new(age_value: 200) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end 境界値分析を用いたテストケース(抜粋)
  23. 51 context "青年パターン、 30歳の場合" do let(:age) { ::Plain::Age.new(age_value: 30) }

    it "幼年と出力されること " do expect(age.output_view_text).to eq("青年") end end context "壮・老年パターン、 31歳の場合" do let(:age) { ::Plain::Age.new(age_value: 31) } it "壮・老年 と出力されること " do expect(age.output_view_text).to eq("壮・老年") end end context "壮・老年パターン、 50歳の場合" do let(:age) { ::Plain::Age.new(age_value: 50) } it "壮・老年 と出力されること " do expect(age.output_view_text).to eq("壮・老年") end end context "壮・老年パターン、 130歳の場合" do let(:age) { ::Plain::Age.new(age_value: 130) } it "壮・老年 と出力されること " do expect(age.output_view_text).to eq("壮・老年") end end context "131歳の場合" do let(:age) { ::Plain::Age.new(age_value: 131) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end context "200歳の場合" do let(:age) { ::Plain::Age.new(age_value: 200) } it "エラーになること " do expect { age.output_view_text }.to raise_error(StandardError) end end 境界値分析を用いたテストケース(抜粋) このテストコードでは130と131の 2パターンテストしており、テストが失敗するので if文の記述ミスに気づくことができる
  24. まとめ 53 • テストコードを含めた自動テストは『仕様の通りに動いているかどう かをいつでも確かめることができる』という性質がある • 自動テストは不具合に早く気付け、修正コストを最小限にすることに 貢献できるのが嬉しいポイント • テストコードを書く際には仕様通りかをチェックできるものを作るの

    が大事 • 仕様を確認するテストコードを書くときにはテストケースをつくると きのテスト技法を使うと効率よくケースをつくることができる • 同値分割法と境界値分析は使いやすい方法なので日頃から頭に入れて おくと便利
  25. 54 しんくう / shinkufencer   @shinkufencer コード日進月歩 https://shinkufencer.hateblo.jp/ ASTERセミナー標準テキスト|Kouichi Akiyama|note

    https://note.com/akiyama924/m/m6981810393cd Software Design 2022年3月号|技術評論社 https://gihyo.jp/magazine/SD/archive/2022/202203 ソフトウェアテスト技法練習帳 ~知識を経験に変える40問~ https://gihyo.jp/book/2020/978-4-297-11061-1 Thanks! 参考にしたサイト/書籍など: