単体テストを学ぼう!③ -テストしたモジュールを1つのシステムにしてみる-
こんにちは、ふじもんです。
健康診断を受けてきました。
腹囲を測ったら去年よりも5cm大きくなってました。
・・・まじか。
さて、前回の記事に引き続き今回もテスト駆動開発で未実装の部分を実装します。
そして、今回はそれらを1つのシステムとして完成させます!
前回のおさらい
前回はどんなシステムを作るか仕様を決め、それに沿ってテスト駆動開発という手法で単体テストを行いながら実装を行なっていました。
そして、前回は1の部分までを実装済みです。
1. 身長(cm)と体重(kg)をキーボードから入力する。 ・身長、体重共に有効な値の範囲は 0 < x < 1000 とし、Trueを返す ・0 以下、もしくは1000以上の数値が入力された場合はFalseを返す ・入力がない場合はFalseを返す ・数値に変換できない文字列が入力された場合はFalseを返す 2. 入力された身長(cm)と体重(kg)からBMIを計算する。
今回は復習も兼ねて2の部分を実装していこうと思います。
BMI計算部分の実装
復習ということでさくさくっと実装していきます!
前回と同じく、テストコードを書いていきます。
2の工程では計算が正しくできていることをテストします。
今回も上記の「入力された身長(cm)と体重(kg)からBMIを計算する」というだけではテストコードを書くのに不十分と気づいたため、仕様を少し詳細に決定しました。
2. 入力された身長(cm)と体重(kg)からBMIを計算する。 ・BMIは、一般的な定義に基づいて算出する。(体重(kg) / 身長(m)^2) ・計算したBMIは小数点第二位で四捨五入し、小数点第一位までを返す。
…早くこういう仕様を最初から決められるようになりたい。
また、入力値は前回まででテスト済みのため、今回のテストでは観点は以下としています。
・取りうる最小の値での計算結果は正しいか
・取りうる最大の値での計算結果は正しいか
・取りうる値の代表値での計算結果は正しいか
・メソッドが呼ばれなかった際の処理は正しいか
この観点でテストコードを書くと以下のようになります。
# bmi_test.py import sys import pathlib sys.path.append('..') current_dir = pathlib.Path(__file__).resolve().parent sys.path.append(str(current_dir) + '/../') from unittest import TestCase from bmi_calculator.BmiCalculator import BmiCalculator class TestBmiCalculator(TestCase): def test_calc_01(self): bmiCalc = BmiCalculator() bmiCalc.set_height("0.1") # 取りうる値の最小値を入力 bmiCalc.set_weight("0.1") self.assertEqual(bmiCalc.calc(), 100000) def test_calc_02(self): bmiCalc = BmiCalculator() bmiCalc.set_height("999.9") # 取りうる値の最小値を入力 bmiCalc.set_weight("999.9") self.assertEqual(bmiCalc.calc(), 10.0) def test_calc_03(self): bmiCalc = BmiCalculator() bmiCalc.set_height("170") # 一般的な日本人男性の身長と体重 bmiCalc.set_weight("60") self.assertEqual(bmiCalc.calc(), 20.8) def test_calc_04(self): bmiCalc = BmiCalculator() bmiCalc.set_height("170") # set_weightメソッドが呼ばれなかった場合 self.assertIsNone(bmiCalc.calc()) def test_calc_05(self): bmiCalc = BmiCalculator() bmiCalc.set_weight("60") # set_heightメソッドが呼ばれなかった場合 self.assertIsNone(bmiCalc.calc()) def test_calc_06(self): bmiCalc = BmiCalculator() # set_weightメソッド、set_heightメソッドが呼ばれなかった場合 self.assertIsNone(bmiCalc.calc())
それぞれのBMIを計算して気づいたのですが、身長と体重が同じでも身長が高ければ高いほど痩せ気味で低ければ低いほど太り気味になるんですね。
計算式見れば当たり前と思う方もいるかもですが、私はここで初めて気づきました。
ではプロダクションコードで実装する前に、テストコードを実行します。
bash-3.2$ python -m unittest bmi_test.py EEEEEE …python (省略) ====================================================================== ERROR: test_calc_06 (bmi_test.TestBmiCalculator) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/XXXX/work/UnitTest/test/bmi_test.py", line 106, in test_calc_06 self.assertIsNone(bmiCalc.calc()) AttributeError: 'BmiCalculator' object has no attribute 'calc' ---------------------------------------------------------------------- Ran 6 tests in 0.001s FAILED (errors=6)
レッドであることを確認できました。
では、テストを通るようにBMI計算部分を実装していきます。
# BmiCalculator.py import re import math class BmiCalculator: INPUT_PATTERN = r"^[\d]+[..]?[\d]?$" def __init__(self): self.height = None self.weight = None self.bmi = None def set_height(self, h): if not re.match(self.INPUT_PATTERN, h): return False h = float(h) if not 0 < h < 1000: return False self.height = h return True def set_weight(self, w): if not re.match(self.INPUT_PATTERN, w): return False w = float(w) if not 0 < w < 1000: return False self.weight = w return True def calc(self): if(self.height is None or self.weight is None): return None return round(self.weight / (self.height / 100) ** 2, 1)
新たにcalc
関数を追加し、height
やweight
に値が入っていない場合は何も返さず、値が設定されていた時のみBMIを計算して返します。
また、__init__
関数で、明示的にそれぞれの初期値をNoneに設定しておきます。
そしてテストコードを実行。
bash-3.2$ python -m unittest bmi_test.py ...... ---------------------------------------------------------------------- Ran 6 tests in 0.001s OK
エラーなし!失敗なし!
これで全ての機能のテストと実行が完了しましたやったーーーーー!!!
システムを完成させよう
と、ここまでやったら満足しがちな私ですが、
これだとそれぞれの機能のモジュールを作っただけになってしまいます。
せっかくテストして動作確認を行なったコードが書けたのだから、
システムとして動かせるようにしたい。
最後に作成したモジュールを繋げるmain.py
を書いて、ちゃんとシステムとして完成させましょう!
# main.py import sys import pathlib sys.path.append('..') current_dir = pathlib.Path(__file__).resolve().parent sys.path.append(str(current_dir) + '/../') from bmi_calculator.BmiCalculator import BmiCalculator if __name__ == '__main__': # BmiCalculatorの生成 bmi = BmiCalculator() # isHeightSuccessの初期値設定 isHeightSuccess = False # isHeightSuccessがTrueになる(有効値が入力される)まで繰り返す while(not isHeightSuccess): print('Input height(cm) = ') height = input() isHeightSuccess = bmi.set_height(height) # isWeightSuccessの初期値設定 isWeightSuccess = False # isWeightSuccessがTrueになる(有効値が入力される)まで繰り返す while(not isWeightSuccess): print('Input weight(kg) = ') weight = input() isWeightSuccess = bmi.set_weight(weight) # BMIの計算 bmi_value = bmi.calc() # BMIが正しく計算されなかった場合はメッセージを表示して終了 if(bmi_value is None): print("Please input height and weight") exit() # 入力された身長・体重と、計算されたBMIを表示 print('height(cm): {} weight(kg): {}'.format(height, weight)) print('BMI: {}'.format(bmi_value))
こんな感じでmain.py
を書いてみました。
身長・体重ともに、有効値を入力したらTrue, 無効値を入力したらFalseを返す仕様にしていたため、有効値が入力されるまで入力メソッドをループさせます。
また、もし今後仕様変更があった場合を考慮し、BMIが正しく計算されなかった場合の処理を入れています。
これが動けば、どんなに単純なものでも立派なシステムよ…!
期待と不安の入り混じった気持ちで実行。
bash-3.2$ python main.py Input height(cm) = 157.8 Input weight(kg) = 53.8 height(cm): 157.8 weight(kg): 53.8 BMI: 21.6
動いたアアアアアアアアアア!!!!!!!!!!!!!!
最後に
テストのテの字も知らずスクリプトを組んでは不安になっていた私ですが、今回テストのことを勉強して、その不安の払拭方法を知ることができました。
テスト…偉大だ…。
また、テスト駆動開発なるテストファーストな手法で行うと、今どんなシステムを作ろうとしているのか、機能は何が必要なのかを明確化でき、開発途中で迷子にならずに最後までシステムが構築できたかと思います。
何よりテストをすることにより不安が払拭され心理的負担が減るのが本当に大きいです。
今回例に出したシステムは非常に単純なシステムだったので、どんな観点でテストを行うかが考えやすかったのですが、
現場に出るともっと大規模なシステムや複雑な処理を要するプログラムだったりして、
・何が必要なのか
・どのような要件なのか
が迷子になりがちだったりします。
そのため、常に念頭に置きながらスクリプトを組むと、
おのずとテストの観点もわかり自分のスクリプトに安心できるのではないかなあと思いました。
お知らせ
株式会社スカイウイルでは、2021年度の新卒採用を行います。
私たちは、独自の教育研修制度によって、学習したことを自信をもって社内外に発信できる。
そんなエンジニア集団です。
弊社採用サイトやマイナビ2021にて情報を公開しておりますので、
興味のある方は是非一度、会社説明会へお越しください!
・採用サイト
株式会社スカイウイル 採用サイト