marp | size | theme | paginate | title |
---|---|---|---|---|
true |
14580 |
shin |
true |
SW設計論 |
・SWEBOK
・良い名前をつける ・コメントはない方が良い
・動くの先にある良いプログラム ・良いプログラムとは?
・Don't call us, we'll call you ・goto不要論からの学び ・できないことを増やす
・Complex vs Complicated ・分割統治 ・DRY・KISS・YAGNI
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
人が叩く:マニュアルテスト 機械が叩く:自動テスト
中身を意識する:ホワイトボックス 中身を意識しない:ブラックボックス
機能:単体テスト, 結合テスト, システムテスト 非機能:パフォーマンステスト, 負荷テスト, ..
インタフェースを決める
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に任す
- テストケースの回収・実行
- 検証 assert()
- 実行結果 ok
ng
の回収
- テストのメタデータ付与 (名前等)
バグを減らす一つの方法はコードを書かないこと
@Test @DisplayName("非整列データのソート")
void testSort1() {
List actual = sort([2,3,1]);
assertThat(actual).equalTo([1,2,3]);
}
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 ==================
システム全体のテスト エンドユーザの実利用を想定したテスト
自動化しにくい, 自動テストに向かない
自動検証しにくい事項もある - UIのくずれ, 見た目 - 操作感
自動テスト:システムの細かな部品 手動テスト:システム全体の振る舞い
・SWテストの基本
・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト
・演習
・テストを先に書く ・"Clean code that works"
・良いテスト ・テストは証明ではない
・テストのテクニック
例)無駄処理の排除, 関数化, 変数名の修正, ...
プログラムができたらリファクタリングすべき 良くない状態を放置しない, 負債を貯めない
https://medium.com/@raychongtk/why-is-refactoring-important-2f1e4dec21ab
振る舞いを変えないのが難しい
すぐに壊れるならまだマシ 壊れていることに気づかないケースが最悪 - 新たなバグの混入 - 不要と判断した処理が実は必要だった
リファクタリング前後での等価性を判定したい
もし等価性を自動判定できたら? → リファクタリングを恐れなくて良くなる
- リファクタリング前にテストを用意しておく
- テストが通る状態を保ちつつ内部を改善する
もしテストが落ちたら? → リファクタリング失敗
- def sort(arr)
+ def sort(arr, is_ascending)
一時的なラッパーを作っておく
def sort(arr):
return sort(arr, True)
テストとソースを同時に直さない
- バグが発生する条件・状況を探す
sort([3,1,2,-5]);
→ [1,2,3,-5] // bug!!
- その条件・状況をテストに起こす プログラムの正しい振る舞いを定義 & 自動検証
def test_sort_negative():
l = sort([3,1,2,-5])
assert l == [-5,1,2,3]
- テストがfailすることを確認する
- テストが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
Chef, Puppet, Pulumi
昔のバグが再度現れる現象
プログラム変更時に発生した予期せぬ別の問題 バグを直したら別のバグが出てくる
「レグレッションがおきた」 「デグレした」
バグ修正時に必ず対応するテストを作る プログラム変更時に常にテストを回す
・SWテストの基本
・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト
・演習
・テストを先に書く ・"Clean code that works"
・良いテスト ・テストは証明ではない
・テストのテクニック
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に提出すること 正解がある問題ではないので自由に考えること
# 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>
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
仕様 + 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"
・良いテスト ・テストは証明ではない
・テストのテクニック
- まずはテストを書く
- テストが通るようにプログラムを作る
- テストを通るようになったらリファクタリング
テストを自動検証可能な仕様だとみなす
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
「動くコード」と「良いコード」を分けて考える 分割統治の考え
step1. まずは動くコードを作る step2. 動いたら良くする
| ★
Clean | step2
|
------------ ↑ ------
|
Dirty ☆ → ★
step0 | step1
Doesn't work Work
学生に仕様とテストを配布する - 4つのサブ課題, 各課題にテスト20個程度
学生は仕様に従いテストが通るように実装する 全テストが通れば課題達成
TDD・テストの経験を得られる TDD・テストの恩恵を自然に受けられる - 機能の自動的な検証 - リファクタリング支援
教員の手間が減る
・SWテストの基本
・リファクタリングのためのテスト ・バグ修正のためのテスト ・回帰バグ対策としてのテスト
・演習
・テストを先に書く ・"Clean code that works"
・良いテスト ・テストは証明ではない
・テストのテクニック
バグの検出能力が高い (≒ 網羅率が高い) 読みやすい・保守しやすい
分岐 if
for
を極力使わない
テスト自体のバグを避けるため
繰り返してもOK, DRYでなくても良い
関数化も最低限に
テスト対象の利用方法という側面もある たくさんのことをしない
目的を満たすか?バグがないか? 計算リソースの無駄がないか?
読みやすいか?意図を汲み取れるか?
拡張時の作業は書き換えか?追加か?
main()
vs main()
+sub1()
+sub2()
+sub3()
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
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"
・良いテスト ・テストは証明ではない
・テストのテクニック
パズル的な面白さ 自動化の気持ちよさ 導入が簡単で奥深い
プログラミングを助ける・楽にする技術 実践的で応用の幅が広い 実装ほど難しくない点もプラス
様々なセオリーが存在する 勉強するほどうまくなる