Open7

テスト勉強

expsh13expsh13

UIコンポーネントテスト(結合テスト)

UIコンポーネントライブラリを書くならsotrybookを書いて確認した方が早そう?
→チェック観点が抜けることがあるのでその点が解決されるのであればよさそう?
→複雑なUI変化になるなどドキュメントの役割として使えそう

UI見てもわからない、リンクなどの確認ならアリかも。
でも、ここも普通のテストでもいいかも

アクセシビリティの観点から記載すると、グループとして識別できるようになど考慮することが良いのでメリットがあるかも

スナップショットでhtml文字列として外部ファイルに保存できるのでこれはアリかも。
https://amzn.asia/d/07SwdwVV

expsh13expsh13

テストコード上のロジックを避ける」

テストコード上では、if for のような制御構文や複雑な関数制御といった、独自のロジック制御はできるだけ控えています。テストコード上にロジックが入ってきた場合、「そのロジックが期待通りかは誰が検証するのか?」という問題が生じます。

「"正しい"とは何か」

テストタイトルに、安易に "正しい" という言葉を使うのは避けるようにしています。

https://blog.cybozu.io/entry/2022/11/14/120000?fbclid=IwY2xjawEgHQdleHRuA2FlbQIxMAABHYurmAxXJogoY7T3z9EgUV3ie8erb5MO4ba7Ub9j1A4TYrNcLYHIpmMQDA_aem_YrUA91WVkrHfJVfc_orSpg

expsh13expsh13

TDD

TDDのゴール

動作するきれいなコード。
テストコードはドキュメントとしての性質も求められる。

TDDのサイクル

まず、動作するコードを作り、その後きれいなコードにする。

  1. 最初に失敗するテストを書く。
    失敗させるテストでのエラーであることを確認することで、テストライブラリが正常に動いていることを確認。実装前にやることが重要。Jestでいうと下記。
test("this test will fail", () => {
  fail("This is an intentional failure");
});
  1. テストリストを書く
    何をテストするか網羅的に先に書き出す。実装脳になったら視野が狭くなりがちなので。

  2. テストを一つ選びテストを書く

  • テスト容易性の高いものから選ぶ。一周目は何もない状態なのでテストが重くなってしまいがちのため。
    テスト容易性とは、観測が簡単であること、制御が簡単であること、対象が十分に小さいこと
  • 慣れると重要度の高いもの=容易性が高いものに近づいていく
    完成したらこう使いたいという利用者視点でテストコードを記載する。
  • テストは準備→実行→検証→(後片付け)の構造をもつ。これをTDDでは下から書いていく。
    検証
    期待値から書いていくので軸がぶれないことが重要。実行値はまだ未記入でok
    実行
    作る前に使うことで、使いやすいものであるか検証する。
test("1を渡すと文字列1を返す", () => {
// convert関数は数値を入れて文字列が返ってきて欲しい
  const actual: string = convert(1);
  expect(actual).toBe("1");
});
  1. そのテストを実行して失敗させる(red)
    実装はしていないのでテストは失敗になる。
    実装コードの実行値と期待値が一致しないエラーのみ発生している状態。
    コードは2で既にできてそう。

  2. 目的のコードを書く

  • テストを成功させるためだけのコードを実装する。
    ここではコードの綺麗さは気にせず、必要最小限のコードを最短で実装。
  • 例えば、下記のようなコードを実装し、エラーにならないか確認する。実装コードは間違いがないので(return "1"しているだけなので、コンパイラなどの処理系を疑うことになる。)、テストコードに間違いがあることになる。テストコードのテストコードなどはキリがないものなので、テストコードのバグが発生していないかここで確認。
export const convert = (a: number): string => {
  return "1";
};
  • テストコード、実装コードともに不安がない場合は、明白な実装になるので、4~6をまとめてやってしまう。
  1. 2で書いたテストを成功させる(Green)
    4にてテストコードにも問題ないことを確認している。

  2. テストが通るまでリファクタリングを行う(refactor)
    リファクタリングとは、ソフトウェア外部から見た振る舞いを変えることなく、理解保守が簡単になるようにソフトウェア内部を綺麗にすること。
    TDDはもう少し狭めて、成功しているテストが成功しているままでコードを綺麗にしていくこと。これにより、定量で判断できるので安全に開発を進められる。
    終わりどきは、時間や重複を除去できたらなどある程度できたら終わりに。

  3. 1のリストに改訂があればする

  4. 2~7を繰り返す
    エラーが発生したときデバッグをする必要が出てくるので、下記のようにアサートを複数並べないようにする。(アサーションルーレット)理想的には一つのテストに一つのアサーション。
    また、ドキュメントとして成り立たないことになる。

test("1を渡すと文字列1を返す", () => {
    // 実行 & 検証
    expect(convert(1)).toBe("1");
    expect(convert(1)).toBe("1");
    expect(convert(1)).toBe("1");
    expect(convert(1)).toBe("1");
    expect(convert(1)).toBe("1");
    expect(convert(2)).toBe("2");
  });

上記の場合、下記が望ましい。

test("1を渡すと文字列1を返す", () => {
  // 実行 & 検証
  expect(convert(1)).toBe("1");
});
test("2を渡すと文字列2を返す", () => {
  // 実行 & 検証
  expect(convert(2)).toBe("2");
});

繰り返すときに各テストの不安や自信によって下記の歩幅を使い分ける

  • テスト→仮実装→三角測量→実装
  • テスト→仮実装→実装
  • テスト→明白な実装
  1. テストをドキュメント化する(テストの構造化とリファクタリング)
    抽象度が低いテストコードだけになると、後から見たときに実装コードをみて中身を把握しなければならずドキュメントの役割を果たせていないので、テストリストで作成したツリー構造を参考にテストコードもツリー構造にする。
    テストコードをツリーにしたとき、階層のレベル感があっているか注意する。
- [x] 数を文字列に変換する
  <!-- 上記だとテストが書きづらいので具体的なものに -->
  - [x] 1 を渡すと文字列"1"を返す = 仮実装
  - [x] 2 を渡すと文字列"2"を返す = 三角測量
- [x] 3 の倍数の時は数の代わりに「Fizz」
  - [x] 3 を渡すと文字列"Fizz"を返す = 仮実装 → テストコードに問題がないので三角測量を通さずリファクタリングをする
- [x] 5 の倍数のときは「Buzz」
  - [x] 5 を渡すと文字列"Buzz"を返す = 仮実装 → テストコード、実装コードともに不安がない場合は明白な実装

describe("convert関数は数を文字列に変換する", () => {
  describe("3 の倍数の時は数の代わりに「Fizz」", () => {
    test("3を渡すと文字列'Fizz'を返す", () => {
      // 実行 & 検証
      expect(convert(3)).toBe("Fizz");
    });
  });
  describe("5 の倍数の時は数の代わりに「Buzz」", () => {
    test("5を渡すと文字列'Buzz'を返す", () => {
      // 実行 & 検証
      expect(convert(5)).toBe("Buzz");
    });
  });
  describe("その他の数の場合はそのまま文字列に変換する", () => {
    test("1を渡すと文字列1を返す", () => {
      // 実行 & 検証
      expect(convert(1)).toBe("1");
    });
    test("2を渡すと文字列2を返す", () => {
      // 実行 & 検証
      expect(convert(2)).toBe("2");
    });
  });
});

また、このタイミングで不要なテストコードも消しておく。上記で言うと「2を渡すと文字列2を返す」は三角測量のために書いたテストであり、後から見てもそれが伝わらないかつ、何か意味のあるものとして消すことができないのでこのタイミングで消す。

describe("convert関数は数を文字列に変換する", () => {
  describe("3 の倍数の時は数の代わりに「Fizz」", () => {
    test("3を渡すと文字列'Fizz'を返す", () => {
      // 実行 & 検証
      expect(convert(3)).toBe("Fizz");
    });
  });
  describe("5 の倍数の時は数の代わりに「Buzz」", () => {
    test("5を渡すと文字列'Buzz'を返す", () => {
      // 実行 & 検証
      expect(convert(5)).toBe("Buzz");
    });
  });
  describe("その他の数の場合はそのまま文字列に変換する", () => {
    test("1を渡すと文字列1を返す", () => {
      // 実行 & 検証
      expect(convert(1)).toBe("1");
    });
  });
});

ライブコーディング

問題

1から100までの数をプリントするプログラミングを書け。ただし、3の倍数の時は数の代わりに「Fizz」、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

コード

https://github.com/expsh13/TDD_Boot_Camp_2020

https://www.youtube.com/live/Q-FJ3XmFlT8?si=9ZEm5cOOOHyts3u_

expsh13expsh13

設計

正直なところ明確な答えは持ち合わせておりませんが、少なくとも実装する対象を正しく、はっきりと、明快に理解することが設計に必要である。この例では有限オートマンを使用して、状態遷移図も作成。
→2次元表で考えると設計の抜け漏れが少なくできそう

型付け

ここでは、TDDのサイクルに入る前に型付けを行っている。
型を優先して実装する理由は、次の2つです。

  1. 型が有限オートマトンと相性が良い
  2. 型はランタイムでは存在しないため、実装よりも情報量が小さい

しかし理由の1番目は、状態遷移表がテストケースとなる点からテストにも該当する性質であり、優先する理由とは言えません。
大事なのは理由の2番目です。
情報量が小さいことは、そのまま集中すべきことを絞れることに繋がります。
すなわち認知負荷の低下により、実装時にバグが混入する確率を下げることができるのです。
→ここもテストリストと同様に都度修正したらいいかも?

三角測量

「三角測量」とは簡単に述べると2つのテストケースからコードをあるべき一般的な形に導くテスト駆動開発の重要なテクニックの1つです。

実装する方向性が明らかな時は「三角測量」のような回りくどい実装をしなくてもいいのです。
テスト駆動開発は、枷を課して窮屈に開発するスタイルでは決してなく、むしろちょうどいいスピードで前進し続けられるように開発する手法です。

https://zenn.dev/sum0/articles/07535a9cdd4a62

expsh13expsh13
  • 〜3、25~28付録を読んだ

面白かった章

25章、付録C

メモ

p.22 三角測量はどうやってリファクタリングしたらよいか全くわからないときにしか使わない。
p.213 テストを失敗させて仕事を終えるとスタートの時に何をやっていたか思い出しやすい。

https://amzn.asia/d/bjsRKSk