From 505962364edc2cadcd2020aa60908a5e51d05ec9 Mon Sep 17 00:00:00 2001 From: shinsuke-mat Date: Mon, 22 Jul 2024 15:44:07 +0900 Subject: [PATCH] add src --- lecture-test.md | 231 ++++++++++++++++++++++++++++++++++++--- src/game.py | 45 ++++++++ src/semver.py | 6 + src/test_game.py | 51 +++++++++ src/test_semver.py | 67 ++++++++++++ src/test_semver_param.py | 30 +++++ 6 files changed, 414 insertions(+), 16 deletions(-) create mode 100644 src/game.py create mode 100644 src/semver.py create mode 100644 src/test_game.py create mode 100644 src/test_semver.py create mode 100644 src/test_semver_param.py diff --git a/lecture-test.md b/lecture-test.md index 540b42a..c96e101 100644 --- a/lecture-test.md +++ b/lecture-test.md @@ -90,6 +90,7 @@ title: SW設計論 #15 ・テストのテクニック + --- # SWテスト @@ -272,7 +273,22 @@ test_sort.py::test_sort3 PASSED [100%] # 開発者が知っておくべきトピック集
-テスト編-
-DUMMY +・SWテストの基本 + +・リファクタリングのためのテスト +・バグ修正のためのテスト +・回帰バグ対策としてのテスト + +・演習 + +・テストを先に書く +・_"Clean code that works"_ + +・良いテスト +・テストは証明ではない + +・テストのテクニック + --- # リファクタリング @@ -420,7 +436,21 @@ Chef, Puppet, Pulumi # 開発者が知っておくべきトピック集
-テスト編-
-DUMMY +・SWテストの基本 + +・リファクタリングのためのテスト +・バグ修正のためのテスト +・回帰バグ対策としてのテスト + +・演習 + +・テストを先に書く +・_"Clean code that works"_ + +・良いテスト +・テストは証明ではない + +・テストのテクニック --- @@ -546,12 +576,28 @@ IF = `bool isSemVer(s)` 仕様 = `EBNF` ``` + --- # 開発者が知っておくべきトピック集
-テスト編-
-DUMMY +・SWテストの基本 + +・リファクタリングのためのテスト +・バグ修正のためのテスト +・回帰バグ対策としてのテスト + +・演習 + +・テストを先に書く +・_"Clean code that works"_ + +・良いテスト +・テストは証明ではない + +・テストのテクニック + --- # テストを先に書く @@ -625,12 +671,28 @@ TDD・テストの恩恵を自然に受けられる 教員の手間が減る + --- # 開発者が知っておくべきトピック集
-テスト編-
-DUMMY +・SWテストの基本 + +・リファクタリングのためのテスト +・バグ修正のためのテスト +・回帰バグ対策としてのテスト + +・演習 + +・テストを先に書く +・_"Clean code that works"_ + +・良いテスト +・テストは証明ではない + +・テストのテクニック + --- # 良いテスト @@ -744,6 +806,26 @@ _Program testing can be used to show the presence of bugs, but never to show the 形式手法と比べるとテストは手軽で安価 +--- + +# 開発者が知っておくべきトピック集
-テスト編- +
+ +・SWテストの基本 + +・リファクタリングのためのテスト +・バグ修正のためのテスト +・回帰バグ対策としてのテスト + +・演習 + +・テストを先に書く +・_"Clean code that works"_ + +・良いテスト +・テストは証明ではない + +・テストのテクニック --- # パラメタ化テスト @@ -762,18 +844,143 @@ def test_semver_valid3(): パラメタ化テスト ```py @pytest.mark.parametrize('semver', [ - '1.2.3' - '1.2.99' - '1.2.0' -]) + '1.2.3', + '1.2.99', + '1.2.0']) def test_semver_valid(semver): assert isSemVer(semver) == True ``` --- # モック +プログラムの依存先を置き換える手法 +テストダブルの一種 (スタブ, スパイ, フェイク, ダミー) + +## 題材:じゃんけんゲーム +ランダムな手を出すプレイヤ +```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 +``` + +--- +## 非決定的なプログラムをどうテストする? +```py +def test_random_player(): + player = RandomPlayer() + hand = player.next_hand() + assert # ????????? +``` + + +## 非決定的な部分だけをモック (模倣) する +```py +def test_random_player(mocker): + mocker.patch('random.random', return_value=0.7) + player = RandomPlayer() + hand = player.next_hand() + assert hand == ROCK +``` + +## モック以外の乱数対策 +シードを固定する (可読性に難あり) +統計的にテストする (コスト・安定性に難あり) +乱数に依存性を注入 (テストのためだけの実装) + + +--- +## じゃんけんゲームのゲームエンジン +```py +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 +``` + +## どうテストする? +```py +def test_game(): + player1 = RandomPlayer() + player2 = RandomPlayer() + game = Game() + winlose = game.play(player1, player2) + assert # ??????? +``` + +--- +## モックありテスト +```py +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 +``` + +## モックありテスト (引き分けの確認) +```py +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"_ + +・良いテスト +・テストは証明ではない + +・テストのテクニック --- # テストは面白い @@ -793,11 +1000,3 @@ def test_semver_valid(semver): 様々なセオリーが存在する 勉強するほどうまくなる ---- -# テストスメル - - -# CI/CD - - -# スタブ・モック \ No newline at end of file diff --git a/src/game.py b/src/game.py new file mode 100644 index 0000000..9e3534b --- /dev/null +++ b/src/game.py @@ -0,0 +1,45 @@ +import random +from enum import Enum + +LEFT_WINS = 1 +RIGHT_WINS = -1 +DRAW = 0 + + +def compare(hand1, hand2): + return hand1 > hand2 + + +class Hand(Enum): + ROCK = 1 + PAPER = 2 + SCISSORS = 3 + + def __lt__(self, other): + if self.value == other.value: + return DRAW + if self.value - other.value == 1 or self.value - other.value == -2: + return RIGHT_WINS + return LEFT_WINS + + +class RandomPlayer(): + def next_hand(self): + r = random.random() + if r > 0.66: + return Hand.ROCK + elif r > 0.33: + return Hand.PAPER + else: + return Hand.SCISSORS + + +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 diff --git a/src/semver.py b/src/semver.py new file mode 100644 index 0000000..ce94fd0 --- /dev/null +++ b/src/semver.py @@ -0,0 +1,6 @@ + +def isSemVer(ver): + import re + numeric = '(0|[1-9][0-9]*)' + pattern = r'^%s.%s.%s$' % (numeric, numeric, numeric) + return re.match(pattern, ver) != None diff --git a/src/test_game.py b/src/test_game.py new file mode 100644 index 0000000..fd84fb2 --- /dev/null +++ b/src/test_game.py @@ -0,0 +1,51 @@ +import pytest +from unittest.mock import MagicMock +from game import * + + +def test_game(): + player1 = RandomPlayer() + player2 = RandomPlayer() + player1.next_hand = MagicMock(return_value=Hand.ROCK) + player2.next_hand = MagicMock(return_value=Hand.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=Hand.ROCK) + player2.next_hand = MagicMock(return_value=Hand.ROCK) + game = Game() + winlose = game.play(player1, player2) + assert winlose == DRAW + + +def test_random_player1(mocker): + mocker.patch('random.random', return_value=0.7) + player = RandomPlayer() + hand = player.next_hand() + assert hand == Hand.ROCK + + +@pytest.mark.skip(reason='affects all random.random due to mock') +def test_random_player2(): + import random + random.random = MagicMock(return_value=0.67) + player = RandomPlayer() + hand = player.next_hand() + assert hand == Hand.ROCK + + +def test_random_player_statistically(): + player = RandomPlayer() + hands = [] + rounds = 1000 + for _ in range(rounds): + hands.append(player.next_hand()) + avg = rounds / 3 + err = rounds / 10 + assert hands.count(Hand.ROCK) > avg - \ + err and hands.count(Hand.ROCK) < avg + err diff --git a/src/test_semver.py b/src/test_semver.py new file mode 100644 index 0000000..bf81060 --- /dev/null +++ b/src/test_semver.py @@ -0,0 +1,67 @@ +import pytest +from semver import * + + +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 + + +def test_semver_valid4(): + assert isSemVer('0.0.0') == True + + +def test_semver_valid5(): + assert isSemVer('10.20.30') == True + + +def test_semver_valid6(): + assert isSemVer( + '99999999999999999.99999999999999999.99999999999999999') == True + + +def test_semver_invalid(): + assert isSemVer('1') == False + + +def test_semver_invalid(): + assert isSemVer('1.2') == False + + +def test_semver_invalid(): + assert isSemVer('1.2.') == False + + +def test_semver_invalid(): + assert isSemVer('1.2.3.') == False + + +def test_semver_invalid(): + assert isSemVer('1..3') == False + + +def test_semver_invalid(): + assert isSemVer('aaa') == False + + +def test_semver_invalid(): + assert isSemVer('1.2.a') == False + + +def test_semver_invalid(): + assert isSemVer('1.01.1') == False + + +def test_semver_invalid(): + assert isSemVer('1. 2.3') == False + + +def test_semver_invalid(): + assert isSemVer('1.-2.3') == False diff --git a/src/test_semver_param.py b/src/test_semver_param.py new file mode 100644 index 0000000..01ad736 --- /dev/null +++ b/src/test_semver_param.py @@ -0,0 +1,30 @@ +import pytest +from semver import * + + +@pytest.mark.parametrize('semver', [ + '1.2.3', + '1.2.99', + '1.2.0', + '0.0.0', + '10.20.30', + '99999999999999999.99999999999999999.99999999999999999', +]) +def test_semver_valid(semver): + assert isSemVer(semver) == True + + +@pytest.mark.parametrize('semver', [ + '1', + '1.2', + '1.2.', + '1.2.3.', + '1..3', + 'aaa', + '1.2.a', + '1.01.1', + '1. 2.3', + '1.-2.3', +]) +def test_semver_invalid(semver): + assert isSemVer(semver) == False