#プログラミング #手法 #TDD #テスト駆動 #着実 #ストレス小
プログラミングにおけるテストについて
プログラムを大量に作成してしまった後、動作させるとか、さらにテストをするとしたとき、『バグが沢山でるのでは?』と、ただならぬ不安はないでしょうか?
また、実際に何度も問題に遭遇していないでしょうか?
テストと聞くと、バグが出て時間と手間がかかり苦労するというイメージをお持ちの方も多いのではないでしょうか?
その考えは本記事で払拭しましょう。
テストは、プログラムが正しく動いて、達成感と称賛をいただき、気持ちよくプログラミングするための味方となります。
先を見越したプログラミング手法
先を見越したプログラミング手法なんてあんの?!
はい、あります。結論から申しますと、テスト駆動開発(TDD)です。
作りたいプログラムを作成しながら、テストプログラムも同時に作り、節目でテストを実施しながら、着実に進めていけます。
また、比較的すぐにテストを行うため、うまく動作しなかった場合でも、その原因となった個所がすぐにわかります。
そのため、長時間悩まされるストレスから開放されるありがたーい開発手法です。
ちなみに、この手法はもう随分前から存在しています。
小生はすぐに取り入れて実践してきており、C言語、Java、PythonやUNIXのbashスクリプトにさえも取り入れて、効果を実感しています。
とにかくプログラミングの手が止まらないこともあるくらいに、達成感を日々感じながら進められるメリットがこの手法にはあります。
その反面、小生の知る現場ではあまり浸透していないように見えています。それゆえに、かえって効果が期待できると確信しています。
TDDを始めてみる
Microsoft Visual Studio Code(*1) + PythonでTDDを始めてみます。
TDD環境の準備
TDD環境の準備を行います。
TDDは、単体テスト(ユニットテスト)(*2)の環境を使って行います。
pythonの場合に限っての内容になりますが、後でハマらないためにも(けっこうハマるんですよ、これが)、下記のフォルダを作ります。
1つの親フォルダ(ここでは”tdd”)の下に、以下の構造で作成します。
classes … テスト対象のプログラムを格納するフォルダ
__init__.py ファイル … TDDに必要なパッケージ(*3)化のための空のファイル
todo_weekday.cpp … テスト対象のプログラム
tests … テストプログラムを格納するフォルダ
__init__.py ファイル … TDDに必要なパッケージ化のための空のファイル
test_todo_weekday.py … テストプログラム
Shift + CTRL + p でコマンドパレットを起動します。
そして、”python test”と入力します。
”Configure Tests”を選択します。
ここでは、python標準の”unittest”を選択します。
フォルダは”tdd”として分けてますので、”tdd”を選択しました。
テストプログラムに付加されるプレフィックス(接頭辞)、サフィックス(接尾辞)を指定します。
ここでは、”test_”のプレフィックスをつけることを選択しました。
テスト対象のプログラムの作成
早速、足がかりとなるプログラムを書いてみます。
とにかく簡単にTDDを実験してみるために、ただ単に “hello!” を出力するプログラムを作成してみました。
def print_hello():
print("hello!")
if __name__ == "__main__" :
print_hello()
しかし! pythonで、外部のテストプログラムからも実行できるようにするためには、わざわざ下記のことを行う必要があります。
・ クラス化する
・ 実行部分をメソッドにする
・ メソッドの第1引数にselfを入れる
なので、少し難しく下記のように。
class Exp1: # class化しなければならない クラス名は先頭を大文字に
def print_hello(self): # クラス化でselfが引数に必要になる
print("hello!")
if __name__ == "__main__" : # テストプログラム無しでも実行できるようにする
Exp1().print_hello() # クラスのオブジェクト(Exp1())を通した呼び出しにする
テストプログラム作成
テストプログラムを作成します。
単にprint_helloを実行するだけですが、いくつかおまじないが必要です。
ユニットテストは、テストスイート(本例題では登場してません)、テストケース、テストメソッドで構成されます。Pythonに限らず、概ね他の言語でも同じであると思います。
import unittest as ut # unittestモジュールをインポート
from classes.exp1 import Exp1 # テストしたいプログラムのインポート
class TestCase1(ut.TestCase): # テストケース
def test_1(self): # テストメソッド
Exp1().print_hello()
if __name__ == "__main__":
ut.main() # ここがテストの入り口
上記テストプログラムを実行します。
以下のどれかで実行できます。
・ プログラムのペイン上のどちらかの”Run Test”をクリックする
・ Shift + CTRL + p でコマンドパレットを起動して”Run All Test”をクリックする
・ 画面下の赤矢印のバーの数字を押すと”Run All Test”が出てくるので、クリックする
以下のように”hello !”とOKが表示され、☓は0となっていれば成功です。
何も表示されていないようであれば、下記画面右のタブが”出力”になっていないと思います。
ユニットテスト自体が開始されないとき
もしテストがうまく開始できない場合は、下記の”launch.json”の設定と、デバッグの種類あたりを疑ってみてください。
小生の場合は、前回の記事でlaunch.jsonをプロセスにアタッチ(接続)してデバッグする内容に書き換えていたため、すんなりと動作しませんでした。
少し気合を入れてTDDをしてみる
前出のプログラムでは、あまりに簡単過ぎますしサンプルとしては参考になりません。
ソースコードを改変してみます。
その前に、単にhello!だけでは、その後が続きません。
そこで、本記事では”今日が何曜日かを求めて、やること(TODO)を表示するプログラム”を作成してみます。
1回目繰り返し:メソッド名変更
クラスは”Exp1″という無意味なものから、”Todo_weekday”へ、そしてメッセージは日本語に変えてみます。
[テスト対象プログラム]
# 今日が何曜日かを求めてやることを表示するプログラム
class Todo_weekday:
# 挨拶をする
def print_hello(self):
print("よう元気か ?!")
if __name__ == "__main__" :
tw = Todo_weekday()
[テストプログラム]
import unittest as ut # unittestモジュールをインポート
from classes.todo_weekday import Todo_weekday # テストしたいプログラムのクラスのインポート
class TestCase1(ut.TestCase): # テストケース
def test_1(self): # テストメソッド
tw = Todo_weekday()
tw.print_hello()
if __name__ == "__main__":
ut.main()
2回目:今日の曜日を出力するよう変更
”今日は~曜日”と表示させたいと思います。
背景色を塗りつぶしたところが変更点です。
[テスト対象プログラム]
# 今日が何曜日かを求めてやることを表示するプログラム
import datetime as dt
class Todo_weekday:
# 挨拶をする
def print_hello(self):
print("よう元気か ?!")
# 今日の曜日を表示する
def print_weekday(self):
today = dt.datetime.today()
weekday_val = today.weekday()
weekday = ['月','火','水','木','金','土','日']
print("今日は '" + weekday[weekday_val] + "' 曜日")
if __name__ == "__main__" :
tw = Todo_weekday()
[テストプログラム]
import unittest as ut # unittestモジュールをインポート
from classes.todo_weekday import Todo_weekday # テストしたいプログラムのクラスのインポート
class TestCase1(ut.TestCase): # テストケース
def test_1(self): # テストメソッド
tw = Todo_weekday()
tw.print_hello()
tw.print_weekday()
if __name__ == "__main__":
ut.main()
実行してみます。
成功してます。順調順調♪
3回目:曜日を求める部分を変更する
今後もこのプログラムを拡張して、曜日にしたがって全く別の処理を追加したりすることが考えられます。
そのため、曜日を求める部分を別のメソッドに変更します。
what_weekday()を追加し、print_weekday()の一部の処理を移動して、returnで曜日の値(0 … 月 ~ 6 … 日)を返すように変更します。
[テスト対象プログラム]
# 今日が何曜日かを求めてやることを表示するプログラム
import datetime as dt
class Todo_weekday:
# 挨拶をする
def print_hello(self):
print("よう元気か ?!")
# 今日の曜日を表示する
def print_weekday(self):
weekday = ['月','火','水','木','金','土','日']
print("今日は '" + weekday[weekday_val] + "' 曜日")
# 今日の曜日を求める
def what_weekday(self):
today = dt.datetime.today()
weekday_val = today.weekday()
return weekday_val
if __name__ == "__main__" :
tw = Todo_weekday()
[テストプログラム]
import unittest as ut # unittestモジュールをインポート
from classes.todo_weekday import Todo_weekday # テストしたいプログラムのクラスのインポート
class TestCase1(ut.TestCase): # テストケース
def test_1(self): # テストメソッド
tw = Todo_weekday()
tw.print_hello()
wd_val = tw.what_weekday()
tw.print_weekday(wd_val)
if __name__ == "__main__":
ut.main()
実行してみます。
ぬおっ! エラーが!
よーくエラーメッセージを見ると実は原因が分からんでもないのですが、分からなかったということにして、ステップ実行してみます。
先程追加したwhat_weekday()が問題の可能性が高いと考えつつ、まずはtest_1メソッドの先頭にブレークポイントを設定します。(行の頭でポチッとするだけ)
次に、TestCase1、またはtest_1の右上に出ている”☓ Debug Test”を押します。実はここを押すことでもテストケース、テストメソッドを実行することができます。
では、矢印にあるステップオーバーを押して進めます。
ステップオーバーとは、今の行にあるメソッドの中には入らずに実行して、次の行に進む実行方法です。メソッドの中を追跡する必要がない場合に使います。
下のタブで、”デバッグコンソール”が選択されていることを確認してください。print_weekday()まで来ましたが、まだ何も起きていません。
そして、次にprint_weekday()を実行すると発生しました。
ということは、print_weekday()に問題があるかもしれないため、ここだけステップイン(メソッドの中まで追跡する)して実行します。
すると、print_weekday()に入る前にエラーが出てしまいました。
こういうときは、メソッドの引数に問題があります。
エラーメッセージを良く見ると、print_weekday() takes 1 positional argument but 2 were given => print_weekday()は1つの位置引数をとるが2つ与えられている
そうです、メソッドの定義に誤りがありました。
変更したときに、引数を1つ増やさなくてはなりませんでした。
しかも、上図の”tw”に赤い波線が出ていて、実行前にエラーがあることを教えてくれていました。
ということで修正します。
[テスト対象プログラム]
# 今日が何曜日かを求めてやることを表示するプログラム
import datetime as dt
class Todo_weekday:
# 挨拶をする
def print_hello(self):
print("よう元気か ?!")
# 今日の曜日を表示する
def print_weekday(self,weekday_val):
weekday = ['月','火','水','木','金','土','日']
print("今日は '" + weekday[weekday_val] + "' 曜日")
# 今日の曜日を求める
def what_weekday(self):
today = dt.datetime.today()
weekday_val = today.weekday()
return weekday_val
if __name__ == "__main__" :
tw = Todo_weekday(
今度は成功しました。
よう元気か?! 今日は ‘日’ 曜日 と表示されています。
ちなみに、”tw”のところが赤波線でした。ここは、上記の修正をしただけでは取れてくれませんでした。ユニットテストの仕様として、結果を残しておくという仕様であると思います。
test_todo_weekday.pyを保存しなおすと、もはや意味がなくなったものと見なされたのか、波線が取れてくれました。
4回目:assertで診断して退行テスト化する
今回(4回目)は、ユニットテストならではのassert()による診断を入れます。
診断を入れていないということは、どういう結果になるのが正しいのかが、テストプログラム中にコード化されていないことを表しています。
そのため、テストを行う度に目視で確認が必要になってしまいます。これでは、テストの自動化に限界があります。
ということで、プログラムを改良します。
print()をself(実行中のクラスのオブジェクト)のprint()に差し替えてしまいます。
そして、画面に表示するだけでなく、saved_textという変数に、表示するテキストを保存します。このようにする理由は、テストプログラムでassertEqual()を使って、画面に表示された文字を診断するためです。
[テスト対象プログラム]
# 今日が何曜日かを求めてやることを表示するプログラム
import datetime as dt
class Todo_weekday:
saved_text = ""
def print(self,text):
self.saved_text = text
print(text)
# 挨拶をする
def print_hello(self):
self.print("よう元気か ?!")
# 今日の曜日を表示する
def print_weekday(self,weekday_val):
weekday = ['月','火','水','木','金','土','日']
self.print("今日は '" + weekday[weekday_val] + "' 曜日")
# 今日の曜日を求める
def what_weekday(self):
today = dt.datetime.today()
weekday_val = today.weekday()
return weekday_val
if __name__ == "__main__" :
tw = Todo_weekday()
[テストプログラム]
import unittest as ut # unittestモジュールをインポート
from classes.todo_weekday import Todo_weekday # テストしたいプログラムのクラスのインポート
class TestCase1(ut.TestCase): # テストケース
def test_1(self): # テストメソッド
tw = Todo_weekday()
tw.print_hello()
self.assertEqual(tw.saved_text,"よう元気か ?!")
wd_val = tw.what_weekday()
tw.print_weekday(wd_val)
if __name__ == "__main__":
ut.main()
テストを実行すると成功します。
もし、assertEqual()で診断する文字が違っていると、テストに失敗します。
そのため、いつしか、”よう元気か ?!”の表示を間違って変えてしまったときに、テストケースを実行するとバグを検出できることになります。
このような過去に正しく動作した結果を保証するためのテストを、一般的にRT(Regression Test=退行テスト)と呼びます。
5回目:最終回
一気に最終回まで行きます。
最終的なコードは以下です。
テストプログラム無しでも、曜日ごとに行うTODOを表示します。
そのために、テストプログラムに書いたコードを抜き出して、”if name == “main” :”の後ろに追加しています。
[テスト対象プログラム]
# 今日が何曜日かを求めてやることを表示するプログラム
import datetime as dt
class Todo_weekday:
saved_text = ""
todo = {0:'歩け', 1:'走れ', 2:'つぶやけ', 3:'仕事しろ', 4:'歩け', 5:'つぶやけ', 6:'ブログ書け'}
def print(self,text):
self.saved_text = text
print(text)
# 挨拶をする
def print_hello(self):
self.print("よう元気か ?!")
# 今日の曜日を表示する
def print_weekday(self,weekday_val):
weekday = ['月','火','水','木','金','土','日']
self.print("今日は '" + weekday[weekday_val] + "' 曜日")
# 今日のTODOを表示する
def print_todo(self, weekday_val):
self.print("さあ " + self.todo[weekday_val] + " !")
# 今日の曜日を求める
def what_weekday(self):
today = dt.datetime.today()
weekday_val = today.weekday()
return weekday_val
if __name__ == "__main__" :
tw = Todo_weekday()
tw.print_hello()
wd_val = tw.what_weekday()
tw.print_weekday(wd_val)
tw.print_todo(wd_val)
テストプログラムのほうは、徹底的に診断コードを追加しました。といっても、テスト対象プログラムからパクって持ってきただけですね。
[テストプログラム]
import unittest as ut # unittestモジュールをインポート
import datetime as dt
from classes.todo_weekday import Todo_weekday # テストしたいプログラムのクラスのインポート
class TestCase1(ut.TestCase): # テストケース
def test_1(self): # テストメソッド
tw = Todo_weekday()
tw.print_hello()
self.assertEqual(tw.saved_text,"よう元気か ?!")
wd_val = tw.what_weekday()
today = dt.datetime.today()
weekday_val = today.weekday()
self.assertEqual(wd_val,weekday_val)
tw.print_weekday(wd_val)
weekday = ['月','火','水','木','金','土','日']
self.assertEqual(tw.saved_text,"今日は '" + weekday[weekday_val] + "' 曜日")
tw.print_todo(weekday_val)
todo = {0:'歩け', 1:'走れ', 2:'つぶやけ', 3:'仕事しろ', 4:'歩け', 5:'つぶやけ', 6:'ブログ書け'}
self.assertEqual(tw.saved_text,"さあ " + todo[weekday_val] + " !")
if __name__ == "__main__":
ut.main()
まとめ
いかがでしたでしょうか。
だいぶ長くなりましたが、TDDを行うことでじっくりと着実に前進できることが分かるかと思います。問題が発生してもすぐに解決でき、ストレス最小限でプログラミングが進みます。
・ TDDの準備をする
・ 少しずつコーディングする
・ テストする
・ 修正する
を繰り返して、気持ちよく歩き続けるようにプログラミングしましょう!
ここに記載されていないケースや解決法についての問い合わせは、ホームページからメールで受け付けています。お気軽にご連絡ください。
コメント