テスト駆動開発について

テスト駆動開発の基礎知識。

TDDとは

汚いコードとは

以下が含まれていると「汚いコード」になる。

  • 密結合
  • 多重ネスト
  • 巨大なクラス
  • 多すぎる引数
  • 多すぎる責務

TDDは分割統治

「動作するきれなコード」を目指して次の2つに分けて近づけていく。

  • きれいなコード
  • 動作するコード

TDDの目的

TDDに必要なこと

  • 問題を小さく分割する
  • 開発の歩幅を調整する
    1. テスト → 仮実装 -> 三角測量 -> 実装
    2. テスト → 仮実装 -> 実装
    3. テスト → 明白な実装
  • テストの構造化
  • リファクタリング
  • テスト作成、テスト実行、実装を素早くテンポよく繰り返す

TDD三大原則

  • 失敗するてユニットテストより先にプロダクションコードを書かない
  • テストケースが適切に失敗するまで次のテストケースを書かない
  • 全てのテストケースが成功するまで次のプロダクションコードを書かない

TDDのサイクル

  1. 目標を考える
    • テキストで目標を書き出す
    • テスト容易性の高いもの、低いもので目標を分ける
    • 目標の中から書くテストをを決める(テスト容易性が低いものを基準に選択)
  2. 目標を示すテストを書く
  3. テストを実行して失敗させる(Red)
    • テスティングフレームワークが正しく動作していることを最初に確認する
    • 最初のテストは設計要素が多いので作業が重い
  4. 目的を達成する最低限のコードを書く
    • コピペでも良いのでとにかく動作させる
  5. テストを成功させる(Green)
    • テストコードのテストは最初に行う(仮実装)
  6. テストが通るまでリファクタリングを行う(Refactor)
    • テストが成功したままで中身を改善する
    • プロダクション、テストどちらのコードも行う
  7. 1-6を繰り返す
    • 繰り返す回数は事前に決める

TDDが苦手なところ

  • DBやUIがテスト実行に制約があるコンポーネントのテスト
  • プロトタイピング等で抜本的な変更が頻繁に行われるコード
  • 処理に時間がかかるコードのテスト

テストのアンチパターン

  • assertは縦に並べない
    • どのテストが失敗しているか分かりにくい
    • テストが失敗すると実行されないテストが発生する
    • アサーションルーレット」と呼ばれる
  • テスト同士に依存性を持たせない
    • 並列分散実行できなくなる

ドキュメントとしてテストを機能させるには

  • テスト名を抽象化しない
    • テスト名を具体性の高いものにする
    • テスト項目をTodoリストと同じような階層構造にする
  • 無意味な重複したテストは削除する
    • 三角測量用のテスト
    • テストの不安を解消するためのテスト

テスト技法について

テスト技法の概要メモ。

ソフトウェアテストやテスト技法の概要

ソフトウェアテスト

  • ソフトウェアが想定通りに動作するか評価、検証すること
  • 仕様通りに機能が動作するのかのチェック

ソフトウェアテストの7つの原則(JSTQBシラバスに記載)

  • テストは欠陥があることは示せる、欠陥が無いことは示せない
    • 不具合は必ずあるもの
  • 全数テストは不可能
    • テスト対象の絞り込み、優先順位が重要
  • 早期テストで時間、コストを節約
    • 不具合の早期発見が大切
  • 欠陥の偏在
    • 不具合は特定の箇所に集中している
    • 過去のデータを参考に不具合箇所を予測する
  • 殺虫座のパラドックス
    • 効果のあるテストを行い続けることで不具合の発生を抑制する
  • テストは状況次第
    • 状況に応じてテストを変化させる必要がある
  • 不具合ゼロの落とし穴
    • 不具合修正の影響を考慮して不具合を修正する

ソフトウェアの品質

  • ユーザーの要求をどれだけ満足させられるか
  • 国際規格 ISO/IED 25010:2011 で定められている

テストの種類

  • 機能テスト
    • リクエスト通りに機能が動作するか
  • 性能テスト
    • ユーザーが快適に仕様できるか
    • レスポンスの速さ
  • 負荷テスト
    • 許容できる限界値が仕様を満たしているか
  • ユーザビリティテスト
    • ユーザーからのフィードバックから課題発見、改善を行う
  • セキュリティテスト

段階毎のテストの分類

  • 詳細設計: 単体テスト
    • 最小単位の機能の動作テスト
  • 基本設計: 結合テスト
    • 機能の組み合わせの動作テスト
  • 要件定義: システムテスト
    • 全ての機能が動作した状態で要件を満たしているのか
  • 要求分析: 受け入れテスト
    • 導入する環境での動作テスト

テスト技法の分類

テスト設計技法

テストケースの作成、選択の手順のこと

テスト設計の流れ

  1. テスト対象のモデル化(状態遷移図、決定表等の作成)
  2. テストモデルの網羅基準を決める
  3. モデル、網羅基準からテストケース導出

テストの適切な定義

  • プログラムのエラーを発見する破壊的な過程である
  • 発見されていないエラーを検出するテストが良いテストケース

テストの原則

  • テストケースは正誤両方を作成する
  • 意図通り、意図されなかった動作の両方のテストが必要

同値分割・境界値分析

同値分割法

  • 同じ意味を持つデータを分類後、分類の代表値を選ぶこと
  • 冗長なテストケースを省ける

境界値分析

  • 同値分割法で導出した代表値との境界値を導出する
  • 境界値付近の不具合を発見しやすくする

効率的なテスト実施の流れ

  1. ブラックボックステスト
  2. テストの網羅性の確認
  3. ホワイトボックステスト

同値分割の流れ

  1. 同値クラスの作成
  2. 同値クラスの分類
  3. 代表値の決定
  4. テスト条件の決定
  5. テスト条件の見直し

同値分割・境界値分析の解説 #テスト技法 - Qiita

デシジョンテーブル

デシジョンテーブルとは?

  • 決定表
  • 入力に対する出力を決定する表

デシジョンテーブルの書き方

  1. 入力(条件)に「Y」、「N」の組み合わせを書き出す
  2. 出力(結果)に「Y」、「- 」を書き出す
  3. 出力のありえない組み合わせは「N/A」を記入

N/Aの意味

デシジョンテーブルの構成

  • 条件記述部
    • condition stub
    • 入力
  • 動作記述部
    • action stub
    • 出力
  • 条件指定部
  • 動作指定部

デシジョンテーブル作成に必要なこと

  • 条件から同値分割の粒度を決める
  • 曖昧な要件を明確化する

デシジョンテーブルの簡単化に必要なこと

  • 不可能な条件の列の排除
  • 可能ではあるが現実的ではない条件を含む列の排除
  • 結果に影響しない条件の列の排除

ホワイトボックステスト

カバレッジ

カバレッジの種類

判定条件/条件網羅

  • condition/dicision coverage
  • DC/CC
  • CDC
  • 全ての条件式の真偽、全ての分岐を実行する

改良条件/判定網羅

復号条件網羅

  • moltiple condition coverage
  • MCC
  • 全ての条件式の真偽の全ての組み合わせのテスト

経路組み合わせ網羅

  • path coverage
  • 命令文、条件式の全ての組み合わせのテスト

システムテスト結合テスト・総合テスト)

テスト作業の構成

  • テスト計画
  • テスト設計
  • テスト実施
  • テスト管理

結合テストの設計の手順

  1. テストシナリオ作成
  2. 一連のテストの流れのパターン化
  3. テストケース定義
  4. どんなテストデータを入力して、どんなテスト結果を想定するか
  5. テストデータ定義
  6. テストケースに応じたテストデータを作成
  7. トランザクションデータも定義

総合テスト

総合テストの手順

  1. 環境構築
  2. 本番マシンのセットアップ
  3. データ移行
  4. マスターデータ等を移行するプログラムの作成
  5. ユーザ教育
  6. マニュアルを使用した操作教育
  7. テストシナリオ実施
  8. 結合テストと同一のシナリオを使用
  9. 本番環境で実施
  10. 運用面のシナリオも追加
  11. 結果の照合・判定
  12. 操作性、機能面をユーザに評価してもらう
  13. 発生した障害に全て対応する

テストケース作成までの手順 1. ゴール確認 - テスト設計: テスト技法でテストケースのパターン抽出 - テストケース作成: 抽出したパターンからテストケース作成 2. テスト設計 3. テストケース作成

Rubyの&:の意味

結論

ブロック処理をより完結に書ける記法。

  • インスタンス.{ |ブロックパラメータ| ブロックパラメータ.メソッド名 }(&:メソッド名) で書ける
  • &:メソッド名 で記述するには特定の条件がある
  • 通称は「あんころ」らしい。

記述例

文字列の入った配列をupcase メソッドで大文字化する処理です。 ブロックを使った記述をを&: で置き換えることができます。

# ブロック使った記述
["a","b","c"].map{ |s| s.upcase }
=> ["A", "B", "C"]

# &:を使った記述
["a","b","c"].map(&:upcase)
=> ["A", "B", "C"]

{ |s| s.upcase } の部分を(&:upcase) に置き換えることができます。

&:メソッド名で書ける条件

以下の条件を満たす場合のみ&:メソッド名 の記述法を使うことができます。 詳しくは「プロを目指すためのRuby入門 4.4.5(p114)」に書かれています。

条件1: ブロックパラメータが1つ

ハッシュに対してブロック処理を行う際はブロックパラメータがキーと値の2つになりますが、ハッシュに対してブロック処理を行う際にブロックパラメータを2つ使って処理をする場合が多いと思いますが、このような場合は&:メソッド名 という書き方はできないようです。

例) ハッシュ内の値(value)のみ大文字にする

# OK
{key_1: "a", key_2: "b", key_3: "c"}.map{ |key, value| value.upcase }
=> ["A", "B", "C"]

# NG
{key_1: "a", key_2: "b", key_3: "c"}.map(&:upcase)
=> undefined method `upcase' for [:key_1, "a"]:Array (NoMethodError)

余談

ハッシュのブロック処理のブロックパラメータの数は必ずキーの2つという決まりは無いようです。

ブロックパラメータ内の値は配列型になっており、ハッシュのブロック処理のブロックパラメータは1つでも正しく動作します。 以下はハッシュのブロックパラメータが1つの時のブロックパラメータの値をコンソールで確認したものです。

> hash = {key_1: "a", key_2: "b", key_3: "c"}
=> {:key_1=>"a", :key_2=>"b", :key_3=>"c"}

# ハッシュでもブロックパラメータは1つでもok
> hash.map{|kv| kv}
=> [[:key_1, "a"], [:key_2, "b"], [:key_3, "c"]]

# ブロックパラメータは配列型で値が代入されている
> hash.map{|kv| kv.class}
=> [Array, Array, Array]

ブロックパラメータkv は配列型で値が保持されています。 ブロックパラメータが2つの場合はこの配列型の値をブロックパラメータのキーと値の2つにそれぞれ代入されているらしいです。

参照: https://pedantic-bardeen-b14a83.netlify.app/1

「ハッシュのブロック処理のブロックパラメータは必ず2つ」という間違った認識を修正できて良かったです。 @haruguchi_yumaさんアドバイスありがとうございました!

条件2: シンボルで渡すメソッドに引数が無い

シンボルで渡すメソッドに引数がある場合は&:メソッド名 という書き方はできないようです。

例) 配列内の文字列を空白で分割する

# OK
["a a", "b b", "c c"].map{ |s| s.split(" ") }
=> [["a", "a"], ["b", "b"], ["c", "c"]]

# NG
["a a", "b b", "c c"].map(&:split(" "))
=> SyntaxError

条件3: ブロックパラメータに対する処理が1つのメソッドのみ

ブロックパラメータに対する処理が1つのメソッドのみでなければなりません。

例) 配列内の文字列を大文字化、空白で分割する

# ブロックパラメータに対するメソッドが複数なので書き換えNG
["a a", "b b", "c c"].map{ |s| s.upcase.split }
=> [["A", "A"], ["B", "B"], ["C", "C"]]

なぜ&:メソッド名で書き換えることができるのか?

例) 配列内の文字列を大文字化する

# ブロック使った記述
["a","b","c"].map{ |s| s.upcase }
=> ["A", "B", "C"]

# &:を使った記述
["a","b","c"].map(&:upcase)
=> ["A", "B", "C"]

理由1: イテレータメソッドに&付きでシンボルを渡すと自動でProc化されるから

イテレータメソッド(eachmap 等)はブロックを受け取ることを期待します。 そしてイテレータメソッドはブロック以外のオブジェクトが渡った場合、ブロック以外で受け取ることができるオブジェクトへの変換を試みます。 この時にブロックから変換されたオブジェクトがProcオブジェクトです。 この自動変換処理はイテレータメソッドに必ずブロックを渡すためのRubyの仕様のようです。

イテレータメソッドに& 付きでシンボルを渡すと暗黙的にto_proc メソッドが呼ばれ、シンボルはProcオブジェクト化されます。 つまり、&:upcase:upcase.to_proc と同じ意味になります。 結果的にただのシンボル からブロック に変換されたことになり、内部的には{ |ブロックパラメータ| ブロックパラメータ.メソッド } と同じ形になっているのだと思います。

手続きオブジェクト(Proc)とは

ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。

メソッド呼び出し(super・ブロック付き・yield) (Ruby 1.9.3)

ブロックの部分だけを先に定義して変数に保存しておき、後からブロック付きメソッドに渡すことも出来ます。 それを実現するのが手続きオブジェクト(Proc)です。

class Proc (Ruby 3.2 リファレンスマニュアル)

ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。

class Proc (Ruby 3.2 リファレンスマニュアル)

Rubyはブロック処理を変数に代入することはできませんが、Procオブジェクトにのインスタンスに変換することでブロック処理をシンボル(:シンボル名) で扱うことができます。 Procオブジェクトはブロック処理を使い回すためのオブジェクトなのだと思います。

余談

to_proc メソッドに手を加えることで& にはシンボル以外の値も渡せるみたいです。 以下はto_proc の引数にメソッド名を文字列を渡せるようにメソッドを再定義しています。

class String
  def to_proc
    Proc.new { |obj| eval "obj.#{self}" }
  end
end

['a', 'b', 'c'].map(&'upcase') # 文字列のupcaseを渡す
=> ['A', 'B', 'C']

イテレータメソッドに&'文字列 でメソッド名を文字列で渡せているのが分かると思います。 Rubyの柔軟さがとても良く分かる面白い例ですね!

引用: https://pedantic-bardeen-b14a83.netlify.app/15?clicks=4

理由2: Procオブジェクトに引数を渡すとシンボルと同名のメソッドを自動で呼び出すから

イテレータメソッドに& 付きでシンボルを渡し、Procオブジェクト化される際にシンボルと同名のメソッドを自動で呼び出す仕様になっているようです。 つまり、&:upcaseupcase メソッドを呼び出します。

  1. map メソッドで配列内から一つずつ値を取り出す
  2. 取り出した値に対してupcase メソッドを実行
  3. 処理した値をmap メソッドで配列として返す

結果的にブロックを使った記述と同じ処理になるので&:メソッド名 で書き換えできるのだと思います。

感想

前に調査した際のメモが残っていたのでもったいないので記事にしてみたシリーズ第二弾(笑) 古いメモを記事に残すのは良い復習になるかもしれない。 Rubyはほんとに奥が深い...

参照

Railsのポリモーフィック関連付けについて

ポリモーフィックとは

1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現できます。

一つのモデルを同じインターフェースを持ったものが扱う(ダックタイピングする)

ポリモーフィックとは1つのモデルで関連付いた複数のモデルの処理を共通化することです。

実装例

例) 本(Book)、日報(Report)からコメント(Comment)を作成する

ポリモーフィックなCommentモデルを作成する

ポリモーフィック用でcommentable カラムを追加する

$ rails g model Comment commentable:references{polymorphic}

上記コマンドでポリモーフィックに必要な以下の2つのカラムが作成される

# schema.rb
...
create_table "comments", force: :cascade do |t|
  t.string "commentable_type", null: false #ポリモーフィック用カラム
  t.integer "commentable_id", null: false #ポリモーフィック用カラム
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
  t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable"
end

関連付け

rails db:migrate 後に以下の関連付けを行う

  • 子モデルCommentにはbelongs_to は自動付与される
  • 親モデルBook、Reportにはhas_many は自動付与されない(手動での付与が必要)
# book.rb
class Book < ApplicationRecord
  has_many :comments, as: :commentable
end

# report.rb
class Report < ApplicationRecord
  has_many :comments, as: :commentable
end

# comment.rb
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

railsコンソールで操作してみる

# ReportからCommentを作成
> report = Report.first
> report.comments.create
=> #<Comment:... id: nil, commentable_type: "Report", commentable_id: 1, created_at: nil, updated_at: nil>

# BookからCommentを作成
> book = Book.first
> book.comments.create
=> #<Comment:... id: nil, commentable_type: "Book", commentable_id: 2, created_at: nil, updated_at: nil>
  • commentable_type にはポリモーフィックで関連付けられたモデル名が入る
  • commentable_id には関連付けられた親モデルのIDが入る

感想

前に調査した際のメモが残っていたのでもったいないので記事にしてみたシリーズ第一弾(笑) 絶対忘れる未来の自分の為にたくさん記事にしていこう。

参照

Active Record の関連付け - Railsガイド

Railsのポリモーフィック関連とはなんなのか - Qiita

Railsのポリモーフィック関連 初心者→中級者へのSTEP10/25 - Qiita

Railsのポリモーフィックでのform_with - karlley

【React】ContextのProviderは正しく囲まないとコンテキストが分岐する

Contextを使用する際のProviderの指定を間違うと共有したはずのコンテキストが子コンポーネント毎に分岐してしまうので注意が必要です。

サンプルコード

以下のようなContextを使って子コンポーネントにログイン情報を共有するコードを例にします。

//Contextファイル
// AuthContext.js

import React, { createContext, useState } from "react";

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const toggleLogin = () => {
    setIsLoggedIn((prev) => !prev);
  };

  return (
    <AuthContext.Provider value={{ isLoggedIn, toggleLogin }}>
      {children}
    </AuthContext.Provider>
  );
};
//子コンポーネント1
//ComponentA.jsx

import React, { useContext } from "react";
import { AuthContext } from "./AuthContext";

const ComponentA = () => {
  const { isLoggedIn, toggleLogin } = useContext(AuthContext);

  return (
    <div>
      <h2>ComponentA</h2>
      <p>ログイン状態: {isLoggedIn ? "ログイン済み" : "未ログイン"}</p>
      <button onClick={toggleLogin}>
        {isLoggedIn ? "ログアウト" : "ログイン"}
      </button>
    </div>
  );
};

export default ComponentA;
//子コンポーネント2
//ComponentB.jsx

import React, { useContext } from "react";
import { AuthContext } from "./AuthContext";

const ComponentB = () => {
  const { isLoggedIn, toggleLogin } = useContext(AuthContext);

  return (
    <div>
      <h2>ComponentB</h2>
      <p>ログイン状態: {isLoggedIn ? "ログイン済み" : "未ログイン"}</p>
      <button onClick={toggleLogin}>
        {isLoggedIn ? "ログアウト" : "ログイン"}
      </button>
    </div>
  );
};

export default ComponentB;

サンプルなので子コンポーネントComponentAComponentB の中身は同じです。

コンテキストが分岐してしまう書き方

以下のような場合は子コンポーネント毎に別々のコンテキストを持つ(分岐してしまう)ことになる。

//親コンポーネント
//App.js
//誤: コンテキストが分岐する

import React from "react";
import { AuthProvider } from "./AuthContext";

const App = () => {
  return (

    <div>
      {/* 子コンポーネントが同じProviderを別々に囲う */}
      <AuthProvider>
        <ComponentA />
      </AuthProvider>
      <AuthProvider>
        <ComponentB />
      </AuthProvider>
    </div>
  );
};
  • isLoggedIn stateが子コンポーネント間で別々のstateとして扱われる
  • コンポーネント内のログインボタンでisLoggedIn stateを更新してもそれぞれの子コンポーネント内のstateしか更新されない
    • 例1) ComponentAでログインボタンクリック → ComponentAのisLoggedIn stateしか更新されない
    • 例2) ComponentBでログインボタンクリック → ComponentBのisLoggedIn stateしか更新されない

結果的にログイン状態を表示するログイン済み/未ログイン の文字の切替、ログインボタンのログイン/ログアウト の文字の切替が子コンポーネント毎にしか動作しなくなります。

コンテキストを正しく共有する書き方

以下のようにコンテクストを共有したいコンポーネントを1つのProvideで囲むことでコンテキストが正しく共有されます。

//親コンポーネント
//App.js
//正: コンテキストが正しく共有される

import React from "react";
import { AuthProvider } from "./AuthContext";

const App = () => {
  return (
    <div>
      {/* 2つの子コンポーネントを1つのProviderで囲う */}
      <AuthProvider>
        <ComponentA />
        <ComponentB />
      </AuthProvider>
    </div>
  );
};
  • 例1) ComponentAでログインボタンクリック → ComponentA、ComponentBのisLoggedIn stateが更新される
  • 例2) ComponentBでログインボタンクリック → ComponentA、ComponentBのisLoggedIn stateが更新される

ログイン状態を表すログイン済み/未ログイン の文字、ログインボタンのログイン/ログアウト の文字がどちらの子コンポーネントでも切り替わるようになります。

まとめ

  • Contextを使ってコンテキストが共有されない場合はProviderが正しく設定されているか疑う
  • 同じコンテキストを共有する複数の子コンポーネントがある場合、1つの親コンポーネント内で1つのProviderで囲むこと
  • 複数のProviderを使用する場合、それぞれが異なる状態を持つ

感想

React、便利なんだろうけど慣れるまでは修羅の道... はやく楽しく触れるようになりたい。

参照

コンテクストで深くデータを受け渡す – React

useContext – React