【Python】単体試験 / unittest ~ 基本編 ~

■ はじめに

https://dk521123.hatenablog.com/entry/2019/10/02/223658

の続き。

目次

【1】Tips
 1)基本的なテンプレート
 2)出力値の確認
 3)例外に関する試験
【2】サンプル
 1)簡単なクラスの単体試験
 2)メソッドの単体試験
 3)例外の単体試験
 4)継承したクラスの単体試験&setUp/tearDown

【1】Tips

1)基本的なテンプレート

import unittest

# from 【対象ファイル】 import 【対象メソッド】
from sample import is_prime

# clsss 【ファイル名】Test(unittest.TestCase):
class SampleTest(unittest.TestCase):

  #   テストを書いていく
  def test_is_prime_yes(self):
    for i in [2, 3, 5, 7, 11, 13, 17, 19]:
      self.assertTrue(is_prime(i))

  # ...

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

2)出力値の確認

構文

# 期待値と同じかどうか
self.assertEqual(【値1】, 【値2】)

# boolean
self.assertTrue(【Boolean値】)
self.assertFalse(【Boolean値】)

3)例外に関する試験

構文

with self.assertRaises(【例外】):
  # 例外を出すメソッドをコール

  def test_is_prime_raise_error(self):
    with self.assertRaises(TypeError):
      is_prime('dummy')

参考文献
https://hiroronn.hatenablog.jp/entry/20181004/1538611164

【2】サンプル

1)簡単なクラスの単体試験

calculator.py (単体試験の対象クラス)

class Calculator(object):
    def plus(self, x, y):
        return x + y

calculator_test.py

import unittest
import calculator

class CalculatorTest(unittest.TestCase):
    def test_plus(self):
        claz = calculator.Calculator()
        self.assertEqual(claz.plus(2, 1), 3)
        
if __name__ == '__main__':
    unittest.main()

出力結果(成功時)

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

出力結果(「self.assertEqual(claz.plus(2, 1), 4)」と修正し意図的に失敗させた場合)

======================================================================
FAIL: test_plus (__main__.CalculatorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "c:\Users\Daisuke\Documents\Python Scripts\calculator_test.py", line 7, in test_plus
    self.assertEqual(claz.plus(2, 1), 4)
AssertionError: 3 != 4

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)

2)メソッドの単体試験

test_file_path_utils.py

import unittest
# 対象のPythonファイル
import file_path_utils


class FilePathUtilsTest(unittest.TestCase):
  def test_convert_file_extention(self):
    file_name = 'hello.world.txt'
    extention = 'csv'
    expected = 'hello.world.csv'
    actual = file_path_utils.convert_file_extention(file_name, extention)
    self.assertEqual(expected, actual)


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

file_path_utils.py

import os

def convert_file_extention(file_name, extention):
  file_name_without_extention = os.path.splitext(file_name)[0]
  return "{}.{}".format(file_name_without_extention, extention)

3)例外の単体試験

calculator.py

def add(num1, num2):
  if (not num1 or not num2):
    raise ValueError('input is None')
  return num1 + num2

calculator.py

import unittest
import calculator

class CalculatorTest(unittest.TestCase):
  def test_add(self):
    self.assertEqual(calculator.add(2, 1), 3)
    
    with self.assertRaises(ValueError):
      calculator.add(None, 1)
    
    with self.assertRaises(ValueError):
      calculator.add(1, None)

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

4)継承したクラスの単体試験&setUp/tearDown

[親クラス] base_demo_class.py

class BaseDemoClass(object):
  def __init__(self):
    self.base_value = 'World'

  def say_hello(self):
    return f'Hello {self.base_value}!'

  def say_hi(self, name):
    return f'Hi, {name}'

[テスト対象クラス] demo_class.py

from base_demo_class import BaseDemoClass

class DemoClass(BaseDemoClass):
  def __init__(self):
    super().__init__()
    self.value = 'Sam'

  def say_thank_you(self):
    return f'Thank you, {self.value}!'

# 簡易動作確認用
if __name__ == '__main__':
  print('Start')
  target_class = DemoClass()
  print(target_class.say_thank_you())
  print(target_class.say_hello())
  print(target_class.say_hi('Mike'))
  print('Done')

test_demo_class.py

import unittest

from demo_class import DemoClass


class TestDemoClass(unittest.TestCase):

  # テストクラスが初期化される際に一度だけ呼ばれる
  @classmethod
  def setUpClass(cls):
    print('setUpClass')

  # テストクラスが解放される際に一度だけ呼ばれる
  @classmethod
  def tearDownClass(cls):
    print('tearDownClass')

  # テストメソッドを実行するたびに呼ばれる
  def setUp(self):
    print('setUp')
    self.target_class = DemoClass()

  # テストメソッドの実行が終わるたびに呼ばれる
  def tearDown(self):
    print('tearDown')

  # 1) say_hello
  def test_say_hello(self):
    self.assertEqual(
      self.target_class.say_hello(),
      'Hello World!')

  # 2) say_hi
  def test_say_hi(self):
    self.assertEqual(
      self.target_class.say_hi('Kevin'),
      'Hi, Kevin')

  # 3) say_thank_you
  def test_say_thank_you(self):
    self.assertEqual(
      self.target_class.say_thank_you(),
      'Thank you, Sam!')

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

参考文献

https://code-schools.com/python-unittest/
https://www.python.ambitious-engineer.com/archives/828

関連記事

単体試験 / unittest ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2019/10/02/223658
単体試験 / mox ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2020/01/19/000000
単体試験 / nose ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2020/01/20/221014
単体試験 / pytest ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2020/12/13/224810