技術

PythonでTDD

PythonでTDD(テスト駆動開発)する方法をググったら、標準のunittestの使い方とか、標準ではないけどnoseの使い方とかがたくさん出てきますが、どれもクラスや関数の使い方の説明に終始してて、実際の開発におけるテストケースを書いたソースファイルのディレクトリ構成や具体的なワークフローについて説明してる所はあまり無く。ググり方が悪いんですかね。

柔軟性が高いというかあまりディレクトリ構成やワークフローに影響を受けないから、各自好きにしろってことなのかな。

しかし、どんな方法にも対応出来るとはいっても、良い方法もあれば悪い方法もあるはず。
てことで自分的に良いと思われる方法・構成を考えてみた。
ただし気に入らない点があるので(後述)、後で解決方法を考える。

前提条件というか目標というか

  • テストには標準のunittestのみを利用する(なんとなくテストについては標準じゃないものに頼りたくない)
  • 普通にテストケースを1つ追加するだけで単体のテスト、パッケージ毎のテスト、全体のテストに反映させそれぞれテストの実行をコマンド1つで出来るようにする
  • テストとテスト対象のコードは分離する(テスト対象コードとテストコードが同じソース内にあると、テストを通すためにテストコードの方をいじるという誘惑に負けやすくなるかもしれないため。また「ソースコードが長くなる→可読性が落ちる」という事態を防ぐため。)
  • テストとテスト対象のディレクトリ構成は同じにする

で、考えてみたのが以下のようなディレクトリ構成
もちろんディレクトリ名・ファイル名は適当(だけどいくつかの依存関係はある(後述))
ディレクトリの深さも特に制限はない

source
├── a.py
├── libs
│   ├── __init__.py
│   ├── b.py
│   ├── c.py
│   └── d.py
├── hoge
│   ├── __init__.py
│   ├── e.py
│   ├── f.py
│   ├── g.py
│   └── h.py
├── test.py
└── tests
    ├── __init__.py
    ├── atest.py
    ├── outputdata
    │   └── i.txt
    ├── testdata
    │   └── j.txt
    ├── tests_libs
    │   ├── __init__.py
    │   ├── btest.py
    │   ├── ctest.py
    │   ├── dtest.py
    │   └── support.py
    └── tests_hoge
        ├── __init__.py
        ├── etest.py
        ├── ftest.py
        ├── gtest.py
        └── htest.py

test.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

from unittest import TextTestRunner, TestCase
import tests

class AllTestCase(TestCase):
	def test_all(self):
		ttr = TextTestRunner(verbosity = 2)
		ttr.run(tests.get_all_test_suite())

if __name__ == "__main__":
	atc = AllTestCase()
	atc.test_all()

test.pyのポイント

  • unittest.TestCaseを継承したクラスを含めることでPython2.7から追加されたdiscoverに対応する(python -m unittest discoverというコマンド1つで全体テストが実行出来る)
  • if __name__ == “__main__”:でも実行するようにしてるのでpython test.pyでも全体テストが出来る

testsディレクトリ配下のすべての__init__.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

import os, glob
from unittest import TestSuite, TestLoader, TextTestRunner

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))

def get_all_test_mods():
	'''同階層のテストを全部インポートする'''
	test_mods = []
	pwd = os.getcwd()
	os.chdir(CURRENT_DIR)
	try:
		for file in glob.glob('*test.py'):
			test_mods.append(__import__(os.path.splitext(file.split('/')[-1])[0], globals(), locals(), []))
		return test_mods
	finally:
		os.chdir(pwd)

def get_all_test_pkgs():
	'''テストが定義されたパッケージをインポートする'''
	test_pkgs = []
	pwd = os.getcwd()
	os.chdir(CURRENT_DIR)
	try:
		for file in glob.glob('*/__init__.py'):
			pkg = __import__(file.split('/')[0], globals(), locals(), [])
			if getattr(pkg, 'get_all_test_suite', None):
				test_pkgs.append(pkg)
		return test_pkgs
	finally:
		os.chdir(pwd)

def get_all_test_suite():
	'''すべてのテストが含まれたTestSuiteを返す'''
	all_tests = TestSuite()

	for tm in get_all_test_mods():
		suite = TestLoader().loadTestsFromModule(tm)
		all_tests.addTest(suite)

	for tp in get_all_test_pkgs():
		suite = tp.get_all_test_suite()
		all_tests.addTest(suite)

	return all_tests

if __name__ == "__main__":
	ttr = TextTestRunner(verbosity = 2)
	ttr.run(get_all_test_suite())

__init__.pyのポイント

  • tests配下の__init__.pyを全部上記の内容にすることで、同階層のテストとサブパッケージのテストを再帰的に行える
  • if __name__ == ‘__main__’:でもテストを実行するようにしてるので、例えばpython tests/tests_libs/__init__.pyというコマンド1つでパッケージ毎のテストができる

tests_libs/btest.pyの例

#!/usr/bin/python
# -*- coding: utf-8 -*-

from libs import b
import unittest

class BTestCase(unittest.TestCase):
	'''Bのテストケース'''
	def setUp(self):
		'''テスト前処理'''
		pass

	def tearDown(self):
		'''テスト終了処理'''
		pass

	def test1_hoge(self):
		'''hogeテスト'''
		# ここにテストコードを書く
		pass

	def test2_fuga(self):
		'''fugaテスト'''
		# ここにテストコードを書く
		pass

if __name__ == '__main__':
	unittest.main()

tests_libs/btest.pyの例のポイント

  • 内容が普通のunittest.TestCaseの使い方そのまんま(今回の構成には依存してない)
  • if __name__ == ‘__main__’:でunittest.main()してるので、python tests/tests_libs/btest.pyというコマンド1つで単体テストできる

ディレクトリ名・ファイル名は何でもいいのですが、今回の方法だと以下の依存関係があります。

  • test.py内でtestsパッケージをハードコーディングで指定してる
  • テストソースファイル名は*test.pyという形を想定してる(テストじゃないコードの場合またはunittest.TestCaseを継承したクラスを含んでるがそのテストを実行したくない場合は*test.pyではない形にする)

気に入らない点

  • testsディレクトリ内のサブディレクトリにtests_と付けるのが面倒(tests_じゃなくてもいいんだけど、テスト対象と同じディレクトリ名にしてしまうと、例えばtests_libs/btest.py内でfrom libs import bでテスト対象モジュールがロードできなくなる)
  • パッケージ毎のテストをする際に__init__.pyを指定しなきゃいけないのがなんか嫌
  • tests配下の__init__.pyを全部同じ内容にしておくのは少し面倒だし、同じコードがたくさんあるのもどうかと思う
  • test.pyではTestCaseの中でTestSuiteを実行してるので、全体テストの結果に’Run 1 test〜’と追加されてしまう

一応条件は満たしているものの微妙だ…

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください