0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Diceクラスの不具合を改修するよ(追加テスト編)

Posted at

概要および趣旨

概要

前回実装したDiceクラス↓には大きな欠陥があるらしいよ(記事コメント参照)。

Javaに慣れているとインスタンス変数にしか見えないものが、Pythonの世界ではクラス変数の扱いになるんだって。怖い怖い。

どうまずいの?

不具合を放っておくと、別インスタンスを生成するたびにシード値が勝手に(いや実装通りなんだけど)書き変わり、複数のDiceインスタンスで同じシード値を共有してしまう。
「呼び出し側がシード値Aのつもりで使ってたダイスが実はシード値Bで動いていた」
ってのは確かにまずい。
「プログラムは思った通りに動かない。書いた通りに動く」っていう古の賢人の言葉を思い出したよ。

今回やること

というわけで、前回の実装のダメな点を追加テストで明らかにしたいんだけど、その前段階としてテストコードをDiceクラスのモジュールから分離しようと思う。

なので、今回やることは以下の3つ。

  • テストコードを別モジュールに分離
  • 別モジュールに新たなテストコードを追加
  • 追加したテストコードを動かして不具合を明らかにする

ボリューム満点だよ。

既存テストコードの分離

前回作った「Dice.py」と同じ階層にモジュール「DiceTest.py」を新規作成。

IDEを使ってるなら、ソースフォルダを「src」と「test」に分けてDice.pyをsrc配下、DiceTest.pyをtest配下に置くと見通しが良くなると思う。

DiceTest.pyが作成出来たら、前回記事の

if __name__ == "__main__":

以下の記述をカット&ペースト。
そのままだとビルドエラー(この用語が適切かわからんけど)が発生するので、冒頭に以下のインポート宣言を追加。

from Dice import Dice
import time

結果、以下のコードになっているはず。

DiceTest.py
'''
Created on 2022/10/09

@author: 芸夢 作郎
'''

import time
from Dice import Dice

if __name__ == "__main__":

    # シード値に350を指定する
    diceExplicit1 = Dice(350)
    print('① シード値指定1回目/シード値: [{}]'.format(diceExplicit1.seedValue))
    for i in  range(0, 100, 1):
        # 1000面ダイスを100回転がす
        print(diceExplicit1.roll(1000))
    print()

    time.sleep(2)

    # シード値を指定しない
    diceTimestamp1 = Dice()
    print('② シード値にタイムスタンプを使用1回目/シード値: [{}]'.format(diceTimestamp1.seedValue))
    for i in  range(0, 100, 1):
        # 1000面ダイスを100回転がす
        print(diceTimestamp1.roll(1000))
    print()

    time.sleep(2)

    # シード値に350を指定する
    diceExplicit2 = Dice(350)
    print('③ シード値指定2回目/シード値: [{}]'.format(diceExplicit2.seedValue))
    for i in  range(0, 100, 1):
        # 1000面ダイスを100回転がす
        print(diceExplicit2.roll(1000))
    print()

    time.sleep(2)

    # シード値を指定しない
    diceTimestamp2 = Dice()
    print('④ シード値にタイムスタンプを使用2回目/シード値: [{}]'.format(diceTimestamp2.seedValue))
    for i in  range(0, 100, 1):
        # 1000面ダイスを100回転がす
        print(diceTimestamp2.roll(1000))
    print()

    time.sleep(2)

    # シード値に350を指定する
    diceExplicitTheOtherSide = Dice(350)
    print('⑤ シード値指定3回目(面数を変更)/シード値: [{}]'.format(diceExplicitTheOtherSide.seedValue))
    for i in  range(0, 100, 1):
        # 2000面ダイスを100回転がす
        print(diceExplicitTheOtherSide.roll(2000))
    print()

念のため実行すると、昨日と同様の結果になる。
本体もテストコードもロジックには手を入れていないから当たり前。

昨日は実行結果を貼るのを端折ったけど、今日は貼っておくよ。
後々の比較で必要になるし。

テスト1実行開始
① シード値指定1回目/シード値: [350]
912
372
822
185
412
867
687
321
668
364
(中略)

② シード値にタイムスタンプを使用1回目/シード値: [1665323375739]
496
524
799
570
591
758
754
912
90
807
(中略)

③ シード値指定2回目/シード値: [350]
912
372
822
185
412
867
687
321
668
364
(中略)

④ シード値にタイムスタンプを使用2回目/シード値: [1665323379759]
14
908
172
942
431
861
449
142
29
379
(中略)

⑤ シード値指定3回目(面数を変更)/シード値: [350]
1823
744
1644
369
823
1733
1374
642
1335
727
(以下略)

新たなテストコードを追加

複数のダイスを同時に転がして出目を横並びで確認するテストケースを追加するよ。
期待値は「シード値と出目の最大値が同じであれば、必ず先日と同様の結果が返ってくること(=各ダイスのシード値が相互に干渉していないこと)」。
確認するためのコードは以下の通り。

	# シード値に350を指定する
	dice350A = Dice(350)
	time.sleep(2)
	# シード値を指定しない
	diceTimestamp1 = Dice()
	time.sleep(2)
	# シード値に350を指定する
	dice350B = Dice(350)
	time.sleep(2)
	# シード値を指定しない
	diceTimestamp2 = Dice()
	time.sleep(2)
	# シード値に700を指定する
	dice700 = Dice(700)

	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print('ロール{}回目'.format(i + 1))
		print('シード値350のダイスロール1/シード値: [{}] 出目: [{}]'.format(dice350A.seedValue, dice350A.roll(1000)))
		print('シード値350のダイスロール2/シード値: [{}] 出目: [{}]'.format(dice350B.seedValue, dice350B.roll(1000)))
		print('シード値にタイムスタンプを使用したダイスロール1/シード値: [{}] 出目: [{}]'.format(diceTimestamp1.seedValue, diceTimestamp1.roll(1000)))
		print('シード値にタイムスタンプを使用したダイスロール2/シード値: [{}] 出目: [{}]'.format(diceTimestamp2.seedValue, diceTimestamp2.roll(1000)))
		print('シード値700のダイスロー2/シード値: [{}] 出目: [{}]'.format(dice700.seedValue, dice700.roll(1000)))
		print()
	print()

何も考えずにDiceTest.pyの末尾に追記してもいいんだけど、確認観点が違うので関数化して別管理したいよね。

というわけで、2つのテストコードを関数化したDiceTest.pyは以下の通り。

DiceTest.py
'''
Created on 2022/10/09

@author: 芸夢 作郎
'''

import time
from Dice import Dice

def test1():
	print('テスト1実行開始')
	# シード値に350を指定する
	diceExplicit1 = Dice(350)
	print('① シード値指定1回目/シード値: [{}]'.format(diceExplicit1.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceExplicit1.roll(1000))
	print()

	time.sleep(2)

	# シード値を指定しない
	diceTimestamp1 = Dice()
	print('② シード値にタイムスタンプを使用1回目/シード値: [{}]'.format(diceTimestamp1.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceTimestamp1.roll(1000))
	print()

	time.sleep(2)

	# シード値に350を指定する
	diceExplicit2 = Dice(350)
	print('③ シード値指定2回目/シード値: [{}]'.format(diceExplicit2.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceExplicit2.roll(1000))
	print()

	time.sleep(2)

	# シード値を指定しない
	diceTimestamp2 = Dice()
	print('④ シード値にタイムスタンプを使用2回目/シード値: [{}]'.format(diceTimestamp2.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceTimestamp2.roll(1000))
	print()

	time.sleep(2)

	# シード値に350を指定する
	diceExplicitTheOtherSide = Dice(350)
	print('⑤ シード値指定3回目(面数を変更)/シード値: [{}]'.format(diceExplicitTheOtherSide.seedValue))
	for i in  range(0, 100, 1):
		# 2000面ダイスを100回転がす
		print(diceExplicitTheOtherSide.roll(2000))
	print()
	return

def test2():
	print('テスト2実行開始')
	# シード値に350を指定する
	dice350A = Dice(350)
	time.sleep(2)
	# シード値を指定しない
	diceTimestamp1 = Dice()
	time.sleep(2)
	# シード値に350を指定する
	dice350B = Dice(350)
	time.sleep(2)
	# シード値を指定しない
	diceTimestamp2 = Dice()
	time.sleep(2)
	# シード値に700を指定する
	dice700 = Dice(700)

	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print('ロール{}回目'.format(i + 1))
		print('シード値350のダイスロール1/シード値: [{}] 出目: [{}]'.format(dice350A.seedValue, dice350A.roll(1000)))
		print('シード値350のダイスロール2/シード値: [{}] 出目: [{}]'.format(dice350B.seedValue, dice350B.roll(1000)))
		print('シード値にタイムスタンプを使用したダイスロール1/シード値: [{}] 出目: [{}]'.format(diceTimestamp1.seedValue, diceTimestamp1.roll(1000)))
		print('シード値にタイムスタンプを使用したダイスロール2/シード値: [{}] 出目: [{}]'.format(diceTimestamp2.seedValue, diceTimestamp2.roll(1000)))
		print('シード値700のダイスロー2/シード値: [{}] 出目: [{}]'.format(dice700.seedValue, dice700.roll(1000)))
		print()
	print()
	return

if __name__ == "__main__":
	test1()
	test2()

テスト

確認したいのはテスト2の実行結果。
コンソールログを確認すると、結果は以下の通り。

テスト2実行開始
ロール1回目
シード値350のダイスロール1/シード値: [350] 出目: [171]
シード値350のダイスロール2/シード値: [350] 出目: [925]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [197]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [459]
シード値700のダイスロー2/シード値: [700] 出目: [717]

ロール2回目
シード値350のダイスロール1/シード値: [350] 出目: [221]
シード値350のダイスロール2/シード値: [350] 出目: [855]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [544]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [630]
シード値700のダイスロー2/シード値: [700] 出目: [811]

ロール3回目
シード値350のダイスロール1/シード値: [350] 出目: [332]
シード値350のダイスロール2/シード値: [350] 出目: [871]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [6]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [622]
シード値700のダイスロー2/シード値: [700] 出目: [862]

(中略)

ロール18回目
シード値350のダイスロール1/シード値: [350] 出目: [504]
シード値350のダイスロール2/シード値: [350] 出目: [501]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [168]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [159]
シード値700のダイスロー2/シード値: [700] 出目: [605]

ロール19回目
シード値350のダイスロール1/シード値: [350] 出目: [625]
シード値350のダイスロール2/シード値: [350] 出目: [666]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [407]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [187]
シード値700のダイスロー2/シード値: [700] 出目: [567]

ロール20回目
シード値350のダイスロール1/シード値: [350] 出目: [154]
シード値350のダイスロール2/シード値: [350] 出目: [428]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [988]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [444]
シード値700のダイスロー2/シード値: [700] 出目: [845]

(以下略)

あれあれあれ、テスト1の実行結果と全然違う。
この乱数列は一体何だろう。

前稿の指摘通り、Diceクラス内のクラス変数が悪さをしているのかな。

というわけで、以下のコードをtest1に追加して実行してみる。

	time.sleep(2)

	# シード値に700を指定する
	diceSeedVal700 = Dice(700)
	print('⑥ シード値700/シード値: [{}]'.format(diceSeedVal700.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceSeedVal700.roll(1000))
	print()

結果は以下の通り。

⑥ シード値700/シード値: [700]
171
925
197
459
717
221
855
544
630
811
332
871
6
622
862
(中略)
504
501
168
159
605
625
666
407
187
567
154
428
988
444
845

較べれば判るとおり、テスト2のロール1回目~20回目の出目の並びと、テスト1(追加)の出目の並びが完全に一致している。

シード値の異なる複数のダイスを並列で転がしていたつもりが、シード値700(最後に指定したシード値)のダイス1個(見た目複数に見える)を一生懸命転がしていたことになる。

確かにこれはおかしい。

おわりに

というわけで、次稿でこの不具合を改修するよ。

今回コードの改修が多かったので、各モジュールの最終形を共有するよ。

Dice.py
'''
Created on 2022/10/08

@author: 芸夢 作郎
'''

import random
import time

class Dice():
    # TODO インスタンス変数を使用するよう改修
    seedValue = None

    def __init__(self, aSeedValue = None):
        if (aSeedValue == None):
            self.seedValue = int(time.time() * 1000)
        else:
            self.seedValue = aSeedValue
        random.seed(self.seedValue)
        return

    # TODO 最大面数(aMaxSidesNum)に整数値以外が指定された場合の対処法も考慮
    def roll(self, aMaxSidesNum):
        ret = random.randint(1, aMaxSidesNum)
        return ret

DiceTest.py
'''
Created on 2022/10/09

@author: 芸夢 作郎
'''

import time
from Dice import Dice

def test1():
	print('テスト1実行開始')
	# シード値に350を指定する
	diceExplicit1 = Dice(350)
	print('① シード値指定1回目/シード値: [{}]'.format(diceExplicit1.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceExplicit1.roll(1000))
	print()

	time.sleep(2)

	# シード値を指定しない
	diceTimestamp1 = Dice()
	print('② シード値にタイムスタンプを使用1回目/シード値: [{}]'.format(diceTimestamp1.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceTimestamp1.roll(1000))
	print()

	time.sleep(2)

	# シード値に350を指定する
	diceExplicit2 = Dice(350)
	print('③ シード値指定2回目/シード値: [{}]'.format(diceExplicit2.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceExplicit2.roll(1000))
	print()

	time.sleep(2)

	# シード値を指定しない
	diceTimestamp2 = Dice()
	print('④ シード値にタイムスタンプを使用2回目/シード値: [{}]'.format(diceTimestamp2.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceTimestamp2.roll(1000))
	print()

	time.sleep(2)

	# シード値に350を指定する
	diceExplicitTheOtherSide = Dice(350)
	print('⑤ シード値指定3回目(面数を変更)/シード値: [{}]'.format(diceExplicitTheOtherSide.seedValue))
	for i in  range(0, 100, 1):
		# 2000面ダイスを100回転がす
		print(diceExplicitTheOtherSide.roll(2000))
	print()

	time.sleep(2)

	# シード値に700を指定する
	diceSeedVal700 = Dice(700)
	print('⑥ シード値700/シード値: [{}]'.format(diceSeedVal700.seedValue))
	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print(diceSeedVal700.roll(1000))
	print()
	return

def test2():
	print('テスト2実行開始')
	# シード値に350を指定する
	dice350A = Dice(350)
	time.sleep(2)
	# シード値を指定しない
	diceTimestamp1 = Dice()
	time.sleep(2)
	# シード値に350を指定する
	dice350B = Dice(350)
	time.sleep(2)
	# シード値を指定しない
	diceTimestamp2 = Dice()
	time.sleep(2)
	# シード値に700を指定する
	dice700 = Dice(700)

	for i in  range(0, 100, 1):
		# 1000面ダイスを100回転がす
		print('ロール{}回目'.format(i + 1))
		print('シード値350のダイスロール1/シード値: [{}] 出目: [{}]'.format(dice350A.seedValue, dice350A.roll(1000)))
		print('シード値350のダイスロール2/シード値: [{}] 出目: [{}]'.format(dice350B.seedValue, dice350B.roll(1000)))
		print('シード値にタイムスタンプを使用したダイスロール1/シード値: [{}] 出目: [{}]'.format(diceTimestamp1.seedValue, diceTimestamp1.roll(1000)))
		print('シード値にタイムスタンプを使用したダイスロール2/シード値: [{}] 出目: [{}]'.format(diceTimestamp2.seedValue, diceTimestamp2.roll(1000)))
		print('シード値700のダイスロー2/シード値: [{}] 出目: [{}]'.format(dice700.seedValue, dice700.roll(1000)))
		print()
	print()

	return

if __name__ == "__main__":
	test1()
	test2()


ではまた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?