2023/02/07

Python unittest

最近在幫公司開發,使用 python 製作出 Windows 使用的小工具,順便 k 了一下單元測試,把遇到過的技巧做個筆記。

chan.py

import os
import shutil

import requests as requests


class Chan:
    def add(self, number1: int, number2: int) -> int:
        return number1 + number2

    def raise_method(self, result: str) -> str:
        if result != 'raise':
            return result

        raise Exception('exception raised')

    def a(self) -> str:
        return self.b()

    def b(self) -> str:
        return 'b'

    def move_file(self, from_path, to_path) -> object:
        return os.rename(from_path, to_path)

    def get_url(self, url) -> int:
        response = requests.get(url)

        return response.status_code

    def my_open(self, path) -> str:
        with open(path) as f:
            return f.read()

    def copy_twice(self, source: str, dest_dir: str) -> None:
        base_path = os.path.dirname(__file__)
        shutil.copyfile(source, os.path.join(base_path, dest_dir, 'first'))
        shutil.copyfile(source, os.path.join(base_path, dest_dir, 'second'))

    def scan_folder(self, path) -> list:
        files = []
        for item in os.listdir(os.path.join(path)):
            if os.path.isfile(os.path.join(path, item)):
                files.append(os.path.join(path, item))

        return files

tests/test_chan.py

import os.path
import unittest
from unittest.mock import patch, MagicMock, mock_open, call

from chan import Chan


def mock_response(*args, **kwargs) -> object:
    class Response:
        status_code = 500

    return Response()


class MyTestCase(unittest.TestCase):
    base_path: str

    def setUp(self) -> None:
        self.base_path = os.path.dirname(os.path.dirname(__file__))

    def test_add(self) -> None:
        chan = Chan()
        actual = chan.add(1, 2)
        self.assertEqual(3, actual)

    def test_raise_should_not_happened(self) -> None:
        chan = Chan()
        actual = chan.raise_method('test')
        self.assertEqual('test', actual)

    def test_raise_should_happened(self) -> None:
        chan = Chan()
        self.assertRaises(Exception, chan.raise_method, 'raise')

    def test_raise_error_message(self) -> None:
        chan = Chan()
        with self.assertRaises(Exception) as error:
            chan.raise_method('raise')

        self.assertEqual('exception raised', str(error.exception))

    @patch.object(Chan, 'b', MagicMock(return_value='c'))
    def test_mock_method(self) -> None:
        chan = Chan()
        actual = chan.a()
        self.assertEqual('c', actual)

    @patch.object(Chan, 'b')
    def test_mock_method_called_once_by_injection(self, mock_b: MagicMock) -> None:
        mock_b.return_value = 'c'
        chan = Chan()
        actual = chan.a()
        self.assertEqual('c', actual)
        self.assertTrue(mock_b.called)

    def test_mock_method_called_once_by_with(self) -> None:
        with patch.object(Chan, 'b') as check:
            check.return_value = 'c'
            chan = Chan()
            actual = chan.a()
            self.assertEqual('c', actual)
            check.assert_called_once()

    @patch('chan.os.rename', MagicMock(return_value='moved'))
    def test_os_method(self) -> None:
        chan = Chan()
        actual = chan.move_file('1.txt', '2.txt')
        self.assertEqual('moved', actual)

    @patch('chan.requests.get', MagicMock(side_effect=mock_response))
    def test_requests_get(self) -> None:
        chan = Chan()
        actual = chan.get_url('https://www.google.com')
        self.assertEqual(500, actual)

    @patch('chan.open', mock_open(read_data='ok'))
    def test_open(self) -> None:
        chan = Chan()
        actual = chan.my_open('path')
        self.assertEqual('ok', actual)

    @patch('chan.open', new_callable=mock_open, read_data='ok')
    def test_open_by_injection(self, m) -> None:
        chan = Chan()
        actual = chan.my_open('path')
        self.assertEqual('ok', actual)
        self.assertTrue(m.called)

    @patch('chan.shutil.copyfile')
    def test_copy_twice(self, mock_copyfile: MagicMock) -> None:
        chan = Chan()
        source = 'test.zip'
        chan.copy_twice(source, 'test_path')

        expected = [call(source, os.path.join(self.base_path, 'test_path', 'first')),
                    call(source, os.path.join(self.base_path, 'test_path', 'second'))]

        self.assertEqual(expected, mock_copyfile.call_args_list)
        self.assertEqual(2, mock_copyfile.call_count)

    @patch('chan.os.path.isfile')
    @patch('chan.os.listdir')
    def test_scan_folder(self, mock_listdir: MagicMock, mock_isfile: MagicMock) -> None:
        mock_listdir.return_value = ['dir1', 'file1', 'dir2', 'file2']
        mock_isfile.side_effect = [False, True, False, True]

        chan = Chan()
        test_dir = 'test_dir'
        actual = chan.scan_folder('test_dir')
        expected = [os.path.join(test_dir, 'file1'), os.path.join(test_dir, 'file2')]
        self.assertEqual(expected, actual)


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