Skip to content

Latest commit

 

History

History
1006 lines (772 loc) · 27.1 KB

lecture-test-full.md

File metadata and controls

1006 lines (772 loc) · 27.1 KB
marp size theme paginate title
true
14580
shin
true
SW設計論

ソフトウェア設計論
#15

まつ本


開発者が知っておくべきトピック集
-実装編-

前回

・SWEBOK

・良い名前をつける ・コメントはない方が良い

・動くの先にある良いプログラム ・良いプログラムとは?

Don't call us, we'll call you ・goto不要論からの学び ・できないことを増やす

・Complex vs Complicated ・分割統治 ・DRY・KISS・YAGNI


SWEBOK 目次

全15章

1. SW要求
2. SW設計
3. SW構築          // 前回
4. SWテスティング   // 今日はここ ★★★★
5. SW保守
6. SW構成管理
7. SWエンジニアリング・マネージメント
8. SWエンジニアリングプロセス
9. SWエンジニアリングモデルおよび方法
10. SW品質
11. SWエンジニアリング専門技術者実践規律
12. SWエンジニアリング経済学
13. 計算基礎
14. 数学基礎
15. エンジニアリング基礎

テストは面白い

プログラミングと似た面白さ

パズル的な面白さ 自動化の気持ちよさ 導入が簡単で奥深い

ものすごく役立つ

プログラミングを助ける・楽にする技術 実践的で応用の幅が広い 実装ほど難しくない点もプラス

理論的側面と実践的側面が共存

様々なセオリーが存在する 勉強するほどうまくなる


開発者が知っておくべきトピック集
-テスト編-

・SWテストの基本

・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト

・演習

・テストを先に書く "Clean code that works"

・良いテスト ・テストは証明ではない

・テストのテクニック


ソフトウェアテスト

対象SWが意図通り動くかを検証するプロセス 下流工程の一つ, 実装とほぼ1:1

基本はSWを叩いてみて確認する (⇔ レビュー, 前回演習)

テストの一例:sort(arr) の単体テスト

def test_sort1():
  actual = sort([2,3,1])    # プログラムを叩いてみて
  assert actual == [1,2,3]  # その結果を確認する

def test_sort2():
  actual = sort([1,2,3])
  assert actual == [1,2,3]

def test_sort3():
  actual = sort(None)       # Noneはどうなるか?
  assert actual == None

様々なテスト

誰が叩くのか?

人が叩く:マニュアルテスト 機械が叩く:自動テスト

どういう目線で叩くか?

中身を意識する:ホワイトボックス 中身を意識しない:ブラックボックス

SWの何をテストするか?

機能:単体テスト, 結合テスト, システムテスト 非機能:パフォーマンステスト, 負荷テスト, ..

今日は自動テストに着目


テスト作成の流れ

インタフェースを決める

def sort(arr: list[int]) -> list[int]

実装する

def sort(arr: list[int]) -> list[int] {
  for ...

テストを作る

def test_sort1():
  actual = sort([2,3,1]) 
  assert actual == [1,2,3] 

仕様とIFが決まればテストは作成できる

仕様 + IF ⇒ 実装
仕様 + IF ⇒ テスト

なぜ自動テストが必要なのか?

よくあるテスト (半自動テスト)

if __name__ == '__main__':
  print(sort([2,3,1]));
  print(sort([1,2,3]));
  print(sort([3,2,1,1,1,1,1,1,0]));
1,2,3
1,2,3
0,1,1,1,1,1,1,2,3

検証にコストを要する, ミスする可能性がある

テストに再利用性がない  - その瞬間の正しさしか確認できていない  - 変更時にまた目視で検証するのか?

自動化されていないこと自体が問題


無茶な自動テスト

if __name__ == '__main__':
  # print(sort([2,3,1]));
  if sort([2,3,1]) == [1,2,3]: # 検証も自動化
    print("ok")
  else:
    print("ng")

ここまでやるならテストFWを使おう

テストの共通処理をFWに任す  - テストケースの回収・実行  - 検証 assert()  - 実行結果 ok ng の回収  - テストのメタデータ付与 (名前等)

バグを減らす一つの方法はコードを書かないこと


各種言語のテストFW

Java:*JUnit, TestNG

@Test @DisplayName("非整列データのソート")
void testSort1() {
  List actual = sort([2,3,1]);
  assertThat(actual).equalTo([1,2,3]);
}

Python:*pytest, unittest

def test_sort1():
  assert sort([2,3,1]) == [1,2,3]
$ pytest -v
test_sort.py::test_sort1 PASSED                 [ 33%]
test_sort.py::test_sort2 PASSED                 [ 66%]
test_sort.py::test_sort3 PASSED                 [100%]
================= 3 passed in 0.02s ==================

手動テストにも価値がある

特にE2Eテスト (End to end)

システム全体のテスト エンドユーザの実利用を想定したテスト

自動化しにくい, 自動テストに向かない

自動検証しにくい事項もある  - UIのくずれ, 見た目  - 操作感

向き不向き

自動テスト:システムの細かな部品 手動テスト:システム全体の振る舞い


開発者が知っておくべきトピック集
-テスト編-

・SWテストの基本

・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト

・演習

・テストを先に書く "Clean code that works"

・良いテスト ・テストは証明ではない

・テストのテクニック


リファクタリング

プログラムの振る舞いを変えずに内部を改善

例)無駄処理の排除, 関数化, 変数名の修正, ...

プログラムができたらリファクタリングすべき 良くない状態を放置しない, 負債を貯めない

https://medium.com/@raychongtk/why-is-refactoring-important-2f1e4dec21ab


技術的負債

雑談


リファクタリングによる破壊

プログラムの「振る舞いを変えずに」~

振る舞いを変えないのが難しい

すぐに壊れるならまだマシ 壊れていることに気づかないケースが最悪  - 新たなバグの混入  - 不要と判断した処理が実は必要だった

Q. 振る舞いの維持をどう確認すれば良いか?

リファクタリング前後での等価性を判定したい

もし等価性を自動判定できたら?  → リファクタリングを恐れなくて良くなる


リファクタリングのためのテスト

テストを等価性判定の目安に使う

  1. リファクタリング前にテストを用意しておく
  2. テストが通る状態を保ちつつ内部を改善する

もしテストが落ちたら?  → リファクタリング失敗

リファクタリングでIFが変わる場合は?

- def sort(arr)
+ def sort(arr, is_ascending)

一時的なラッパーを作っておく

def sort(arr):
  return sort(arr, True)

テストとソースを同時に直さない


バグ修正のためのテスト

予期せぬバグが起きた場合:

  1. バグが発生する条件・状況を探す
sort([3,1,2,-5]);
   → [1,2,3,-5]  // bug!!
  1. その条件・状況をテストに起こす プログラムの正しい振る舞いを定義 & 自動検証
def test_sort_negative():
  l = sort([3,1,2,-5])
  assert l == [-5,1,2,3]
  1. テストがfailすることを確認する
  2. テストがpassするようにバグを直す

テストとソースを同時に直さない


バグ報告もテストと同時に

バグ報告に書くべき項目

バグの概要 環境 バグの再現方法 ★ 期待する振る舞い ★ 実際の振る舞い ★

def test_sort_negative():
  l = sort([3,1,2,-5])
  assert l == [-5,1,2,3]

下手な文章よりも役立つ

機械解読可能 = 客観的 コード化 = 自動化・再現可能


何でもソースコードにしよう

雑談

インストール手順のコード化

※Home brewのインストールスクリプト

$ /bin/bash -c "$(curl -fsSL https://raw.git../install.sh)"

ビルド方法・依存解決のコード化

Java: Gradle, Bazel Python: requirements.txt + $ pip install

仮想環境のコード化

Docker

インフラ環境のコード化 (IaC)

Chef, Puppet, Pulumi


回帰バグ対策としてのテスト

回帰バグ

昔のバグが再度現れる現象

プログラム変更時に発生した予期せぬ別の問題 バグを直したら別のバグが出てくる

「レグレッションがおきた」 「デグレした」

テストは回帰バグの特効薬

バグ修正時に必ず対応するテストを作る プログラム変更時に常にテストを回す


開発者が知っておくべきトピック集
-テスト編-

・SWテストの基本

・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト

・演習

・テストを先に書く "Clean code that works"

・良いテスト ・テストは証明ではない

・テストのテクニック


演習 (10m)

以下のプログラムのテストケースを作成せよ

def isSemVer(v: str) -> bool:
  """文字列vがsemverの書式に従っているかを確認する
  semver = majorVer.minorVer.patchVer
  """

テストケース例

assert isSemVer('1.2.3')  == True
assert isSemVer('1.2.99') == True

assert isSemVer('1.2.')  == False
assert isSemVer('1.2.a') == False

提出方法

テストのテキストをCLEに提出すること 正解がある問題ではないので自由に考えること


そもそも正しいsemverとは?

# Backus–Naur Form Grammar for Valid SemVer Versions
<valid semver> ::= <version core>
          | <version core> "-" <pre-release>
          | <version core> "+" <build>
          | <version core> "-" <pre-release> "+" <build>

<version core> ::= <major> "." <minor> "." <patch>

<major> ::= <numeric identifier>
<minor> ::= <numeric identifier>
<patch> ::= <numeric identifier>

<numeric identifier> ::= "0"
          | <positive digit>
          | <positive digit> <digits>
<positive digit> ::= "1" | "2" | "3" | .. | "9"
<digit> ::= "0" | <positive digit>

https://semver.org/


解答例

valid

1.2.3
1.2.99
1.2.0
0.0.0
10.20.30
99999999999999999.99999999999999999.99999999999999999

invalid

1
1.2
1.2.
1.2.3.
1..3
aaa
1.01.1
1. 2.3
1.-2.3

もっと真面目なテストケース

valid

1.1.2-prerelease+meta
1.0.0-alpha
1.0.0-beta
1.0.0-alpha.beta
1.0.0-alpha.beta.1
1.0.0-alpha.1
1.0.0-alpha0.valid
1.0.0-alpha.0valid
1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay
1.0.0-rc.1+build.1
2.0.0-rc.1+build.123
1.2.3-beta
10.2.3-DEV-SNAPSHOT
1.2.3-SNAPSHOT-123
2.0.0+build.1848
2.0.1-alpha.1227

semver/semver#833 (comment)


テスト ≒ 仕様

仕様が決まらないとテストは作れない

仕様 + IF ⇒ 実装
仕様 + IF ⇒ テスト

IF = isSemVer(s: str) -> bool: 仕様 = BNF

テストは自動検証可能な仕様である

コード化された仕様

テストがあると実装が楽 (テストがゴールになる)

実装前の状態: -----------------------    0%
まず軽く実装: oooooooo------oo-------   50%
少し修正する: oooooooooooooooooooo-oo   98%
完成!!!!: ooooooooooooooooooooooo  100%

https://kusumotolab.github.io/lecture-sw-design/src/test_semver.py


開発者が知っておくべきトピック集
-テスト編-

・SWテストの基本

・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト

・演習

・テストを先に書く "Clean code that works"

・良いテスト ・テストは証明ではない

・テストのテクニック


テストを先に書く

TDD - Test driven development, テスト駆動開発

  1. まずはテストを書く
  2. テストが通るようにプログラムを作る
  3. テストを通るようになったらリファクタリング

テストを自動検証可能な仕様だとみなす

1の終了の時点

def test_sort1():
  assert sort([2,3,1]) == [1,2,3]
def test_sort2():
  assert sort([1,2,3]) == [1,2,3]
def test_sort3(): ...
def sort(arr):
  return None

"Clean code that works"

TDDの肝

「動くコード」と「良いコード」を分けて考える 分割統治の考え

step1. まずは動くコードを作る step2. 動いたら良くする

                         |     ★
   Clean                 |    step2
                         |
          ------------ ↑ ------
                         |     
   Dirty         ☆       →     ★
               step0     |    step1

           Doesn't work        Work

論文でも同じことが言える

雑談


Test Driven Class

雑談

テスト駆動の演習 (演習D)

学生に仕様とテストを配布する  - 4つのサブ課題, 各課題にテスト20個程度

学生は仕様に従いテストが通るように実装する 全テストが通れば課題達成

Pros

TDD・テストの経験を得られる TDD・テストの恩恵を自然に受けられる  - 機能の自動的な検証  - リファクタリング支援

教員の手間が減る


開発者が知っておくべきトピック集
-テスト編-

・SWテストの基本

・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト

・演習

・テストを先に書く "Clean code that works"

・良いテスト ・テストは証明ではない

・テストのテクニック


良いテスト

良いテストとは?

バグの検出能力が高い (≒ 網羅率が高い) 読みやすい・保守しやすい

単純であればあるほどよい

分岐 if for を極力使わない テスト自体のバグを避けるため 繰り返してもOK, DRYでなくても良い 関数化も最低限に

1テストケース=1シナリオ

テスト対象の利用方法という側面もある たくさんのことをしない


良いプログラムとは?

再掲

信頼性・効率性 (実行的側面の良さ)

目的を満たすか?バグがないか? 計算リソースの無駄がないか?

可読性・保守性

読みやすいか?意図を汲み取れるか?

拡張性

拡張時の作業は書き換えか?追加か?

テスタビリティ

main() vs main()+sub1()+sub2()+sub3()


Complex vs Complicated

再掲

https://www.gilkisongroup.com/investing-complicated-or-complex/


良いプログラムと良いテスト

テストが楽なプログラムを作ろう

うまく分割統治すればするほどテストが楽 テストが楽なほど実装も楽

分割統治されていない何でもできるメソッド

HugeObject doALotOfThings(param1, param2, param3, ...) {

分割統治された単一のことしかできないメソッド

TinyObject doTinyThing(param1) {

良いプログラム ≒ テストしやすいプログラム

責務が明確である, 具体的で明確な名前を持つ 副作用がない, 状態を持たない, 決定的である

関数型言語はテストしやすい







Program testing can be used to show the presence of bugs, but never to show their absence

E.W. Dijkstra

bg left:45%


テストは証明ではない

全組み合わせのテストは不可能

isSemVer(s) のパラメタの組み合わせは無限

テストは無料ではない

大きなシステムの全テストは1日かかる

テストの経済学と心理学

経済的なテストを作る  - 効率の良さのこと

開発者の心理を考えてテストを作る  - 開発者がミスしやすそうな部分を考える  - 開発経験があるほど良いテストを作れる


プログラムの正しさの証明?

形式手法

雑談

数学的・形式的な分析に基づいて信頼性を確認 システム全体を数学的に表現することで検証する

E.M. Clarke et al, ACM Computing Surveys 1996.

ミッションクリティカル分野で利用されている 航空システム・医療等

形式手法と比べるとテストは手軽で安価


開発者が知っておくべきトピック集
-テスト編-

・SWテストの基本

・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト

・演習

・テストを先に書く "Clean code that works"

・良いテスト ・テストは証明ではない

・テストのテクニック


パラメタ化テスト

テストをDRYにする手法

non-DRYなテスト

def test_semver_valid1():
    assert isSemVer('1.2.3') == True
def test_semver_valid2():
    assert isSemVer('1.2.99') == True
def test_semver_valid3():
    assert isSemVer('1.2.0') == True

パラメタ化テスト

@pytest.mark.parametrize('semver', [
    '1.2.3',
    '1.2.99',
    '1.2.0'])
def test_semver_valid(semver):
    assert isSemVer(semver) == True

https://kusumotolab.github.io/lecture-sw-design/src/test_semver_param.py


モック

プログラムの依存先を置き換える手法 テストダブルの一種 (スタブ, スパイ, フェイク, ダミー)

題材:じゃんけんゲーム

https://kusumotolab.github.io/lecture-sw-design/src/game.py https://kusumotolab.github.io/lecture-sw-design/src/test_game.py

ランダムな手を出すプレイヤ

class RandomPlayer():
    def next_hand(self):
        r = random.random()
        if r > 0.66:
            return ROCK
        elif r > 0.33:
            return PAPER
        else:
            return SCISSORS

非決定的なプログラムをどうテストする?

def test_random_player():
    player = RandomPlayer()
    hand = player.next_hand()
    assert # ?????????

非決定的な部分だけをモック (模倣) する

def test_random_player(mocker):
    mocker.patch('random.random', return_value=0.7)
    player = RandomPlayer()
    hand = player.next_hand()
    assert hand == ROCK

モック以外の乱数対策

シードを固定する (可読性に難あり) 統計的にテストする (コスト・安定性に難あり) 乱数に依存性を注入 (テストのためだけの実装)


じゃんけんゲームのゲームエンジン

class Game():
    def play(self, player1, player2):
        for _ in range(10):
            hand1 = player1.next_hand()
            hand2 = player2.next_hand()
            winlose = compare(hand1, hand2)
            if winlose != DRAW:
                return winlose
        return DRAW

どうテストする?

def test_game():
    player1 = RandomPlayer()
    player2 = RandomPlayer()
    game = Game()
    winlose = game.play(player1, player2)
    assert # ???????

モックありテスト

def test_game():
    player1 = RandomPlayer()
    player2 = RandomPlayer()
    player1.next_hand = MagicMock(return_value=ROCK)
    player2.next_hand = MagicMock(return_value=SCISSORS)
    game = Game()
    winlose = game.play(player1, player2)
    assert winlose == LEFT_WINS

モックありテスト (引き分けの確認)

def test_game_draw():
    player1 = RandomPlayer()
    player2 = RandomPlayer()
    player1.next_hand = MagicMock(return_value=ROCK)
    player2.next_hand = MagicMock(return_value=ROCK)
    game = Game()
    winlose = game.play(player1, player2)
    assert winlose == DRAW

モックまとめ

モックを使うべき状況

モック先の制御が難しい (非決定的) モック先の処理コストが高い (高計算, DB依存, NW依存) モック先が十分にテストされている

やりすぎ注意

実装をねじ曲げる手法ではある なんでもできる (goto文と同じ)

やりすぎると:  - テストの可読性が下がる  - テスト自体のバグにつながる  - プロダクトのバグの見逃しにつながる


開発者が知っておくべきトピック集
-テスト編-

・SWテストの基本

・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト

・演習

・テストを先に書く "Clean code that works"

・良いテスト ・テストは証明ではない

・テストのテクニック


テストは面白い

再掲

プログラミングと似た面白さ

パズル的な面白さ 自動化の気持ちよさ 導入が簡単で奥深い

ものすごく役立つ

プログラミングを助ける・楽にする技術 実践的で応用の幅が広い 実装ほど難しくない点もプラス

理論的側面と実践的側面が共存

様々なセオリーが存在する 勉強するほどうまくなる