Python:unittest
단위 테스트 프레임워크.
unittest 단위 테스트 프레임워크는 본래 JUnit으로부터 영감을 받고 다른 언어의 주요 단위 테스트 프레임워크와 비슷한 특징을 가지고 있습니다. 이것은 테스트 자동화, 테스트를 위한 사전 설정(setup)과 종료(shutdown) 코드 공유, 테스트를 컬렉션에 종합하기, 테스트와 리포트 프레임워크의 분리 등을 지원합니다.
Features
이를 달성하기 위해 unittest는 객체 지향적인 방법으로 몇 가지 중요한 개념을 지원합니다.
- 테스트 픽스쳐
- 테스트 픽스쳐 (test fixture)는 1개 또는 그 이상의 테스트를 수행할 때 필요한 준비와 그와 관련된 정리 동작에 해당합니다. 예를 들어 이것은 임시 또는 프락시 데이터베이스, 디렉터리를 생성하거나 서버 프로세스를 시작하는 것 등을 포함합니다.
- 테스트 케이스
- 테스트 케이스(test case)는 테스트의 개별 단위입니다. 이것은 특정한 입력 모음에 대해서 특정한 결과를 확인합니다. unittest는 베이스 클래스인 TestCase를 지원합니다. 이 클래스는 새로운 테스트 케이스를 만드는 데 사용됩니다.
- 테스트 묶음
- 테스트 묶음(test suite)은 여러 테스트 케이스, 테스트 묶음, 또는 둘 다의 모임입니다. 이것은 서로 같이 실행되어야 할 테스트들을 종합하는 데 사용됩니다.
- 테스트 실행자
- 테스트 실행자(test runner)는 테스트 실행을 조율하고 테스트 결과를 사용자에게 제공하는 역할을 하는 컴포넌트입니다. 실행자는 테스트 실행 결과를 보여주기 위해 그래픽 인터페이스, 텍스트 인터페이스를 사용하거나 특별한 값을 반환할 수도 있습니다.
Discover test
복수개의 파일을 모두 스캔하여 한꺼번에 테스트할 수 있다.
WARNING |
주의할 점은, 테스트 파일명은 test로 시작해야하며, TestCase의 테스트 메서드 이름도 test로 시작해야 한다. 이 것을 변경하고 싶다면 |
Help message
$ python -m unittest discover --help
Usage: python -m unittest discover [options]
Options:
-h, --help show this help message and exit
-v, --verbose Verbose output
-f, --failfast Stop on first fail or error
-c, --catch Catch ctrl-C and display results so far
-b, --buffer Buffer stdout and stderr during tests
-s START, --start-directory=START
Directory to start discovery ('.' default)
-p PATTERN, --pattern=PATTERN
Pattern to match tests ('test*.py' default)
-t TOP, --top-level-directory=TOP
Top level directory of project (defaults to start
directory)
setUpClass vs setUp
setUpClass
는 테스트케이스(클래스)당 1회 호출되고 setUp
는 테스트(함수)당 1회 호출된다. tearDown
과 tearDownClass
도 동일하게 적용된다.
비동기 테스트
직접 테스트케이스 만들기
# -*- coding: utf-8 -*-
import functools
from asyncio import (
AbstractEventLoop,
get_event_loop,
new_event_loop,
set_event_loop,
iscoroutinefunction,
)
from unittest import TestCase
from typing import Callable
COROUTINE_FUNCTION_ALLOWS = ["setUp", "tearDown"]
TEST_FUNCTION_PREFIX = "test_"
def _async_to_sync_decorator(func: Callable, loop: AbstractEventLoop):
"""Sync function for calling async function.
.. deprecated::
Returning as `FunctionTypes` causes pytest's unit tests to fail.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
return loop.run_until_complete(func(*args, **kwargs))
return wrapper
def _get_default_loop(debug=True) -> AbstractEventLoop:
loop: AbstractEventLoop
try:
loop = get_event_loop()
except: # noqa
loop = new_event_loop()
set_event_loop(loop)
loop.set_debug(debug)
return loop
class AsyncTestCase(TestCase):
# We need to pick a loop for entire testing phase,
# otherwise it will trigger create new loops in new threads,
# leads to deadlock.
_test_loop = _get_default_loop()
def __init__(self, method_name="runTest"):
super().__init__(method_name)
@property
def loop(self) -> AbstractEventLoop:
return self._test_loop
def set_debug(self, flag=True) -> None:
self._test_loop.set_debug(flag)
def disable_debug(self) -> None:
self._test_loop.set_debug(False)
@staticmethod
def mangling_tester_name(name: str) -> str:
return "__async_backend_" + name
def exist_mangled_tester(self, name: str) -> bool:
mangled_tester_name = self.mangling_tester_name(name)
try:
return super().__getattribute__(mangled_tester_name) is not None
except AttributeError:
return False
def __getattribute__(self, name: str):
"""Overrides the loading logic to support coroutine functions."""
attr = super().__getattribute__(name)
# If possible, converts the coroutine into a sync function.
if name.startswith(TEST_FUNCTION_PREFIX) or name in COROUTINE_FUNCTION_ALLOWS:
if iscoroutinefunction(attr):
class _Runner:
def __init__(self, loop: AbstractEventLoop, func):
self.loop = loop
self.func = func
def _runner(self):
self.loop.run_until_complete(self.func())
# Returning as `FunctionTypes` causes pytest's unit tests to fail.
return _Runner(self.loop, attr)._runner
# For other attributes, let them pass.
return attr
사용방법은 다음과 같다:
# -*- coding: utf-8 -*-
import unittest
import grpc
from tester.unittest.async_test_case import AsyncTestCase
from recc.proto.rpc import rpc_api_pb2 as api
from recc.proto.rpc.rpc_api_pb2_grpc import RpcApiStub
class RpcNoServerTestCase(AsyncTestCase):
async def setUp(self):
self.host = "localhost"
self.port = 19999
self.client = grpc.aio.insecure_channel(f"{self.host}:{self.port}")
self.assertEqual(grpc.ChannelConnectivity.IDLE, self.client.get_state(True))
async def tearDown(self):
await self.client.close()
async def test_healthcheck(self):
with self.assertRaises(grpc.aio.AioRpcError):
pat = RpcApiStub(self.client).Heartbeat(api.Pit(delay=0), timeout=1.0)
await pat
if __name__ == "__main__":
unittest.main()
여기서 만든 AsyncTestCase
의 문제는, assertion 이 발생될 경우 정확한 위치가 표시되지 않는다.
IsolatedAsyncioTestCase 사용하기
from unittest import IsolatedAsyncioTestCase
events = []
class Test(IsolatedAsyncioTestCase):
def setUp(self):
events.append("setUp")
async def asyncSetUp(self):
self._async_connection = await AsyncConnection()
events.append("asyncSetUp")
async def test_response(self):
events.append("test_response")
response = await self._async_connection.get("https://example.com")
self.assertEqual(response.status_code, 200)
self.addAsyncCleanup(self.on_cleanup)
def tearDown(self):
events.append("tearDown")
async def asyncTearDown(self):
await self._async_connection.close()
events.append("asyncTearDown")
async def on_cleanup(self):
events.append("cleanup")
if __name__ == "__main__":
unittest.main()
테스트를 실행한 후, events에는 ["setUp", "asyncSetUp", "test_response", "asyncTearDown", "tearDown", "cleanup"]
가 포함됩니다.
TestCase
Example
import unittest
class TestArchive(unittest.TestCase):
@classmethod
def setUpClass(cls):
print 'setUp class'
@classmethod
def tearDownClass(cls):
print 'tearDown class'
def setUp(self):
print 'setUp'
def tearDown(self):
print 'tearDown'
def testDefault(self):
self.assertTrue(false)
if __name__ == '__main__':
unittest.main()
Parameterize
unittest_parametrize 패키지가 필요하다. 자세한 내용은 해당 항목 참조.
참고로 pytest의 @pytest.mark.parametrize
데코레이터는 unittest.TestCase 클래스를 상속받는 구조에서는 작동하지 않는다.