Pengenalan sederhana untuk Test Driven Development dengan Python

Saya seorang pengembang pemula otodidak yang mampu menulis aplikasi sederhana. Tapi aku harus membuat pengakuan. Tidak mungkin untuk mengingat bagaimana semuanya saling berhubungan di kepala saya.

Situasi ini menjadi lebih buruk jika saya kembali ke kode yang telah saya tulis setelah beberapa hari. Ternyata masalah ini bisa diatasi dengan mengikuti metodologi Test Driven Development (TDD).

Apa itu TDD dan mengapa itu penting?

Dalam istilah awam, TDD merekomendasikan menulis tes yang akan memeriksa fungsionalitas kode Anda sebelum Anda menulis kode yang sebenarnya. Hanya jika Anda puas dengan pengujian Anda dan fitur-fitur yang diuji, barulah Anda mulai menulis kode sebenarnya untuk memenuhi ketentuan yang diberlakukan oleh pengujian yang memungkinkannya lulus.

Mengikuti proses ini memastikan bahwa Anda merencanakan dengan cermat kode yang Anda tulis untuk lulus tes ini. Ini juga mencegah kemungkinan penundaan tes tulis, karena tes tersebut mungkin tidak dianggap perlu dibandingkan dengan fitur tambahan yang dapat dibuat selama waktu itu.

Pengujian juga memberi Anda kepercayaan diri saat mulai melakukan refactor kode, karena Anda lebih mungkin menemukan bug karena umpan balik instan saat pengujian dijalankan.

Bagaimana memulainya?

Untuk mulai menulis tes dengan Python, kita akan menggunakan unittestmodul yang disertakan dengan Python. Untuk melakukan ini kami membuat file baru mytests.py, yang akan berisi semua tes kami.

Mari kita mulai dengan “halo dunia” yang biasa:

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

Perhatikan bahwa kami mengimpor helloworld()fungsi dari mycodefile. Dalam file mycode.pykami awalnya hanya akan menyertakan kode di bawah ini, yang membuat fungsi tetapi tidak mengembalikan apa pun pada tahap ini:

def hello_world(): pass

Berjalan python mytests.pyakan menghasilkan keluaran berikut di baris perintah:

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)

Ini jelas menunjukkan bahwa tes gagal, yang diharapkan. Untungnya, kami telah menulis pengujian, jadi kami tahu bahwa itu akan selalu ada untuk memeriksa fungsi ini, yang memberi kami kepercayaan diri untuk menemukan potensi bug di masa mendatang.

Untuk memastikan kode lolos, mari ubah mycode.pyke berikut ini:

def hello_world(): return 'hello world'

Berjalan python mytests.pylagi kita mendapatkan output berikut di baris perintah:

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

Selamat! Anda baru saja menulis ujian pertama Anda. Sekarang mari beralih ke tantangan yang sedikit lebih sulit. Kami akan membuat fungsi yang memungkinkan kami membuat pemahaman daftar numerik kustom dengan Python.

Mari kita mulai dengan menulis tes untuk fungsi yang akan membuat daftar panjang tertentu.

Dalam file mytests.pyini akan menjadi metode 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)

Ini akan menguji bahwa fungsi tersebut create_num_listmengembalikan daftar panjang 10. Mari buat fungsi create_num_listdi mycode.py:

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

Berjalan python mytests.pyakan menghasilkan keluaran berikut di baris perintah:

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)

Hal ini seperti yang diharapkan, jadi mari kita pergi ke depan dan berubah fungsi create_num_listdi mytest.pydalam rangka untuk lulus tes:

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

Mengeksekusi python mytests.pydi baris perintah menunjukkan bahwa pengujian kedua sekarang juga telah lulus:

..
--------------------------------------------------------------------
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.