Pythonを使用したテスト駆動開発の簡単な紹介

私は、簡単なアプリを書くことができる独学の初心者開発者です。しかし、私には告白があります。すべてが私の頭の中でどのように相互接続されているかを思い出すのは不可能です。

数日後に書いたコードに戻ると、この状況はさらに悪化します。この問題は、テスト駆動開発(TDD)の方法論に従うことで克服できることがわかりました。

TDDとは何ですか?なぜそれが重要なのですか?

素人の言葉で言えば、TDDは、実際のコードを作成する前に、コードの機能をチェックするテストを作成することをお勧めします。テストとそれがテストする機能に満足した場合にのみ、テストに合格するために課せられた条件を満たすために、実際のコードを書き始めます。

このプロセスに従うことで、これらのテストに合格するために作成するコードを慎重に計画することができます。これにより、テストの作成が後日延期される可能性もなくなります。これは、その間に作成される可能性のある追加機能と比較して、テストが必要であると見なされない可能性があるためです。

また、テストは、コードのリファクタリングを開始するときに自信を与えます。これは、テストの実行時に即座にフィードバックが行われるため、バグを見つける可能性が高くなるためです。

どうやって始めるのか?

Pythonでテストを書き始めるために、Pythonにunittest付属のモジュールを使用します。これを行うために、mytests.pyすべてのテストを含む新しいファイルを作成します。

通常の「HelloWorld」から始めましょう。

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')

ファイルhelloworld()から関数をインポートしていることに注意してくださいmycode。ファイルmycode.pyには、最初に以下のコードを含めるだけです。このコードは関数を作成しますが、この段階では何​​も返しません。

def hello_world(): pass

実行python mytests.pyすると、コマンドラインに次の出力が生成されます。

F
====================================================================
FAIL: test_hello (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 7, in test_hello
self.assertEqual(hello_world(), 'hello world')
AssertionError: None != 'hello world'
--------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)

これは、テストが失敗したことを明確に示していますが、これは予想どおりでした。幸い、すでにテストを作成しているので、この関数をチェックするために常にそこにあることがわかっています。これにより、将来の潜在的なバグを見つけることができます。

コードが確実に合格するようmycode.pyに、次のように変更します。

def hello_world(): return 'hello world'

python mytests.py再度実行すると、コマンドラインに次の出力が表示されます。

.
--------------------------------------------------------------------
Ran 1 test in 0.000s
OK

おめでとうございます!最初のテストを作成しました。それでは、もう少し難しい課題に移りましょう。Pythonでカスタム数値リスト内包表記を作成できるようにする関数を作成します。

特定の長さのリストを作成する関数のテストを作成することから始めましょう。

ファイルでは、mytests.pyこれはメソッドになりますtest_custom_num_list

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world') def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10)

これは、関数create_num_listが長さ10のリストを返すことをテストします。で関数create_num_listを作成しましょうmycode.py

def hello_world(): return 'hello world'
def create_num_list(length): pass

実行python mytests.pyすると、コマンドラインに次の出力が生成されます。

E.
====================================================================
ERROR: test_custom_num_list (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 14, in test_custom_num_list
self.assertEqual(len(create_num_list(10)), 10)
TypeError: object of type 'NoneType' has no len()
--------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (errors=1)

これは予想通りで、それでは、先に行くと変更機能せcreate_num_listmytest.pyテストに合格するためには:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]

python mytests.pyコマンドラインで実行すると、2番目のテストも合格したことがわかります。

..
--------------------------------------------------------------------
Ran 2 tests in 0.000s
OK

Let’s now create a custom function that would transform each value in the list like this: const * ( X ) ^ power . First let’s write the test for this, using method test_custom_func_ that would take value 3 as X, take it to the power of 3, and multiply by a constant of 2, resulting in the value 54:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')
def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10) def test_custom_func_x(self): self.assertEqual(custom_func_x(3,2,3), 54)

Let’s create the function custom_func_x in the file mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): pass

As expected, we get a fail:

F..
====================================================================
FAIL: test_custom_func_x (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 17, in test_custom_func_x
self.assertEqual(custom_func_x(3,2,3), 54)
AssertionError: None != 54
--------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)

Updating function custom_func_x to pass the test, we have the following:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power

Running the tests again we get a pass:

...
--------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

Finally, let’s create a new function that would incorporate custom_func_x function into the list comprehension. As usual, let’s begin by writing the test. Note that just to be certain, we include two different cases:

import unittestfrom mycode import *
class MyFirstTests(unittest.TestCase):
def test_hello(self): self.assertEqual(hello_world(), 'hello world')
def test_custom_num_list(self): self.assertEqual(len(create_num_list(10)), 10)
def test_custom_func_x(self): self.assertEqual(custom_func_x(3,2,3), 54)
def test_custom_non_lin_num_list(self): self.assertEqual(custom_non_lin_num_list(5,2,3)[2], 16) self.assertEqual(custom_non_lin_num_list(5,3,2)[4], 48)

Now let’s create the function custom_non_lin_num_list in mycode.py:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power
def custom_non_lin_num_list(length, const, power): pass

As before, we get a fail:

.E..
====================================================================
ERROR: test_custom_non_lin_num_list (__main__.MyFirstTests)
--------------------------------------------------------------------
Traceback (most recent call last):
File "mytests.py", line 20, in test_custom_non_lin_num_list
self.assertEqual(custom_non_lin_num_list(5,2,3)[2], 16)
TypeError: 'NoneType' object has no attribute '__getitem__'
--------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (errors=1)

In order to pass the test, let’s update the mycode.py file to the following:

def hello_world(): return 'hello world'
def create_num_list(length): return [x for x in range(length)]
def custom_func_x(x, const, power): return const * (x) ** power
def custom_non_lin_num_list(length, const, power): return [custom_func_x(x, const, power) for x in range(length)]

Running the tests for the final time, we pass all of them!

....
--------------------------------------------------------------------
Ran 4 tests in 0.000s
OK

Congrats! This concludes this introduction to testing in Python. Make sure you check out the resources below for more information on testing in general.

The code is available here on GitHub.

Useful resources for further learning!

Web resources

Below are links to some of the libraries focusing on testing in Python

25.3. unittest - Unit testing framework - Python 2.7.14 documentation

The Python unit testing framework, sometimes referred to as "PyUnit," is a Python language version of JUnit, by Kent…docs.python.orgpytest: helps you write better programs - pytest documentation

The framework makes it easy to write small tests, yet scales to support complex functional testing for applications and…docs.pytest.orgWelcome to Hypothesis! - Hypothesis 3.45.2 documentation

It works by generating random data matching your specification and checking that your guarantee still holds in that…hypothesis.readthedocs.iounittest2 1.1.0 : Python Package Index

The new features in unittest backported to Python 2.4+.pypi.python.org

YouTube videos

If you prefer not to read, I recommend watching the following videos on YouTube.