-
fulfillPython
-
12-unittest
Notebook
샘플 코드는 최종 파일만 배포합니다
(주:booksearch_module.py)
import unittest
# 애플리케이션 코드
def booksearch():
# 임의 처리
return {}
class BookSearchTest(unittest.TestCase):
# booksearch() 테스트 코드
def test_booksearch(self):
self.assertEqual({}, booksearch())
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
test_booksearch (booksearch_module.BookSearchTest) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
import unittest
# 애플리케이션 코드
def booksearch():
# 임의 처리
return {}
class BookSearchTest(unittest.TestCase):
# booksearch() 테스트 코드
def test_booksearch(self):
self.assertEqual({}, booksearch())
if __name__ == '__main__':
unittest.main()
test_booksearch (__main__.BookSearchTest) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
/Users/yeonsookim/Workspaces/personal/python-src/12-unittest/workspace
$ python3 -m unittest
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
from .core import Book, get_books
__all__ = ['Book', 'get_books']
import json
from urllib import request, parse
def get_json(param):
with request.urlopen(build_url(param)) as f:
return json.load(f)
def get_data(url):
with request.urlopen(url) as f:
return f.read()
def build_url(param):
query = parse.urlencode(param)
return ('https://www.googleapis.com'
f'/books/v1/volumes?{query}')
import imghdr
import pathlib
from .api import get_data, get_json
class Book:
"""API 응답의 VolumeInfo 엘리먼트에 대응"""
def __init__(self, item):
self.id = item['id']
volume_info = item['volumeInfo']
for k, v in volume_info.items():
setattr(self, str(k), v)
def __repr__(self):
return str(self.__dict__)
def save_thumbnails(self, prefix):
"""섬네일 이미지를 저장함"""
paths = []
for kind, url in self.imageLinks.items():
thumbnail = get_data(url)
# 이미지 데이터로부터 확장자 판정
ext = imghdr.what(None, h=thumbnail)
# pathlib.Path는 / 연산자로 경로를 추가할 수 있음
base = pathlib.Path(
prefix) / f'{self.id}_{kind}'
filename = base.with_suffix(f'.{ext}')
filename.write_bytes(thumbnail)
paths.append(filename)
return paths
def get_books(q, **params):
"""서적 검색 수행"""
params['q'] = q
data = get_json(params)
return [Book(item) for item in data['items']]
---------------------------------------------------------------------------
SSLCertVerificationError Traceback (most recent call last)
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py in do_open(self, http_class, req, **http_conn_args)
1318 try:
-> 1319 h.request(req.get_method(), req.selector, req.data, headers,
1320 encode_chunked=req.has_header('Transfer-encoding'))
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py in request(self, method, url, body, headers, encode_chunked)
1229 """Send a complete request to the server."""
-> 1230 self._send_request(method, url, body, headers, encode_chunked)
1231
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py in _send_request(self, method, url, body, headers, encode_chunked)
1275 body = _encode(body, 'body')
-> 1276 self.endheaders(body, encode_chunked=encode_chunked)
1277
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py in endheaders(self, message_body, encode_chunked)
1224 raise CannotSendHeader()
-> 1225 self._send_output(message_body, encode_chunked=encode_chunked)
1226
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py in _send_output(self, message_body, encode_chunked)
1003 del self._buffer[:]
-> 1004 self.send(msg)
1005
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py in send(self, data)
943 if self.auto_open:
--> 944 self.connect()
945 else:
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py in connect(self)
1398
-> 1399 self.sock = self._context.wrap_socket(self.sock,
1400 server_hostname=server_hostname)
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ssl.py in wrap_socket(self, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, session)
499 # ctx._wrap_socket()
--> 500 return self.sslsocket_class._create(
501 sock=sock,
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ssl.py in _create(cls, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, context, session)
1039 raise ValueError("do_handshake_on_connect should not be specified for non-blocking sockets")
-> 1040 self.do_handshake()
1041 except (OSError, ValueError):
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ssl.py in do_handshake(self, block)
1308 self.settimeout(None)
-> 1309 self._sslobj.do_handshake()
1310 finally:
SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1108)
During handling of the above exception, another exception occurred:
URLError Traceback (most recent call last)
<ipython-input-6-c93f24622be7> in <module>
1 from booksearch import get_books
----> 2 books = get_books(q='python')
~/Workspaces/personal/python-src/12-unittest/workspace/booksearch/core.py in get_books(q, **params)
35 """서적 검색 수행"""
36 params['q'] = q
---> 37 data = get_json(params)
38 return [Book(item) for item in data['items']]
~/Workspaces/personal/python-src/12-unittest/workspace/booksearch/api.py in get_json(param)
3
4 def get_json(param):
----> 5 with request.urlopen(build_url(param)) as f:
6 return json.load(f)
7
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py in urlopen(url, data, timeout, cafile, capath, cadefault, context)
220 else:
221 opener = _opener
--> 222 return opener.open(url, data, timeout)
223
224 def install_opener(opener):
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py in open(self, fullurl, data, timeout)
523
524 sys.audit('urllib.Request', req.full_url, req.data, req.headers, req.get_method())
--> 525 response = self._open(req, data)
526
527 # post-process response
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py in _open(self, req, data)
540
541 protocol = req.type
--> 542 result = self._call_chain(self.handle_open, protocol, protocol +
543 '_open', req)
544 if result:
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py in _call_chain(self, chain, kind, meth_name, *args)
500 for handler in handlers:
501 func = getattr(handler, meth_name)
--> 502 result = func(*args)
503 if result is not None:
504 return result
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py in https_open(self, req)
1360
1361 def https_open(self, req):
-> 1362 return self.do_open(http.client.HTTPSConnection, req,
1363 context=self._context, check_hostname=self._check_hostname)
1364
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py in do_open(self, http_class, req, **http_conn_args)
1320 encode_chunked=req.has_header('Transfer-encoding'))
1321 except OSError as err: # timeout error
-> 1322 raise URLError(err)
1323 r = h.getresponse()
1324 except:
URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1108)>
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-7-483e878a896e> in <module>
1 # 실행 시 얻은 데이터에 따라 결과가 달라짐
----> 2 books[0]
NameError: name 'books' is not defined
(주:tests/test_api.py)
import unittest
class BuildUrlTest(unittest.TestCase):
def test_build_url(self):
# build_url()이 테스트 대상 처리
from booksearch.api import build_url
expected = 'https://www.googleapis.com/books/v1/volumes?q=python'
actual = build_url({'q': 'python'})
# assert 메서드 이용
self.assertEqual(expected, actual)(주:tests/test_api.py)
(생략)
actual = build_url({'q': 'python'})
# assert 메서드 이용
self.assertEqual(expected, actual)
# 좋지 않은 예시
# 위 행이 실패하면 아래 행이 실행되지 않음
expected2 = 'https://www.googleapis.com/books/v1/volumes?'
actual2 = build_url({})
self.assertEqual(expected2, actual2)(주:tests/test_api.py)
(생략)
actual = build_url({'q': 'python'})
# assert 메서드 이용
self.assertEqual(expected, actual)
# 좋은 예
def test_build_url_empty_param(self):
from booksearch.api import build_url
expected = 'https://www.googleapis.com/books/v1/volumes?'
actual = build_url({})
self.assertEqual(expected, actual)
(주:tests/test_core.py)
import pathlib
import unittest
from tempfile import TemporaryDirectory
THUMBNAIL_URL = (
'http://books.google.com/books/content'
'?id=OgtBw76OY5EC&printsec=frontcover'
'&img=1&zoom=1&edge=curl&source=gbs_api'
)
class SaveThumbnailsTest(unittest.TestCase):
def setUp(self):
# 임시 디렉터리 작성
self.tmp = TemporaryDirectory()
def tearDown(self):
# 임시 디렉터리 정리
self.tmp.cleanup()
def test_save_thumbnails(self):
from booksearch.core import Book
book = Book({'id': '', 'volumeInfo': {
'imageLinks': {
'thumbnail': THUMBNAIL_URL
}}})
# 처리를 실행하고 파일이 작성되었음을 확인
filename = book.save_thumbnails(self.tmp.name)[0]
self.assertTrue(pathlib.Path(filename).exists())
$ python3 -m unittest
...
----------------------------------------------------------------------
Ran 3 tests in 0.899s
OK$ python3 -m unittest -v
test_build_url (test_api.BuildUrlTest) ... ok
test_build_url_empty_param (test_api.BuildUrlTest) ... ok
test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.337s
OK
(주:tests/test_api.py)
class BuildUrlTest(unittest.TestCase):
(생략)
def test_build_url_fail(self):
from booksearch.api import build_url
expected = 'https://www.googleapis.com/books/v1/volumes'
actual = build_url({})
self.assertEqual(expected, actual,
msg='이 테스트는 실패합니다')$ python3 -m unittest -v
test_build_url (test_api.BuildUrlTest) ... ok
test_build_url_empty_param (test_api.BuildUrlTest) ... ok
test_build_url_fail (test_api.BuildUrlTest) ... FAIL
test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok
======================================================================
FAIL: test_build_url_fail (test_api.BuildUrlTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/.../workspace/tests/test_api.py", line 24, in test_build_url_fail
msg='이 테스트는 실패합니다')
AssertionError: 'https://www.googleapis.com/books/v1/volumes' != 'https://www.googleapis.com/books/v1/volumes?'
- https://www.googleapis.com/books/v1/volumes
+ https://www.googleapis.com/books/v1/volumes?
? +
: 이 테스트는 실패합니다
----------------------------------------------------------------------
Ran 4 tests in 0.397s
FAILED (failures=1)
(주:tests/test_api.py)
class BuildUrlTest(unittest.TestCase):
(생략)
@unittest.expectedFailure
def test_build_url_fail(self):$ python3 -m unittest -v
test_build_url (test_api.BuildUrlTest) ... ok
test_build_url_empty_param (test_api.BuildUrlTest) ... ok
test_build_url_fail (test_api.BuildUrlTest) ... expected failure
test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.400s
OK (expected failures=1)
# 모듈 지정
$ python3 -m unittest -v tests.test_api
test_build_url (tests.test_api.BuildUrlTest) ... ok
test_build_url_empty_param (tests.test_api.BuildUrlTest) ... ok
test_build_url_fail (tests.test_api.BuildUrlTest) ... expected failure
----------------------------------------------------------------------
Ran 3 tests in 0.049s
OK (expected failures=1)
# 클래스 지정
$ python3 -m unittest -v tests.test_api.BuildUrlTest
(생략)
# 메서드 지정
$ python3 -m unittest -v tests.test_api.BuildUrlTest.test_build_url
(생략)
# 여러 테스트 지정
$ python3 -m unittest -v tests.test_api tests.test_core
(생략)
# 실행할 테스트 모듈 이름을 지정
$ python3 -m unittest discover -s tests -p 'test_c*.py' -v
test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.362s
OK
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-14-a88ddb0aa40f> in <module>
1 # 예외 클래스나 그 인스턴스에서는 해당 예외가 전송됨
2 mock.side_effect = ValueError('에러입니다')
----> 3 mock()
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py in __call__(self, *args, **kwargs)
1073 self._mock_check_sig(*args, **kwargs)
1074 self._increment_mock_call(*args, **kwargs)
-> 1075 return self._mock_call(*args, **kwargs)
1076
1077
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py in _mock_call(self, *args, **kwargs)
1077
1078 def _mock_call(self, /, *args, **kwargs):
-> 1079 return self._execute_mock_call(*args, **kwargs)
1080
1081 def _increment_mock_call(self, /, *args, **kwargs):
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py in _execute_mock_call(self, *args, **kwargs)
1132 if effect is not None:
1133 if _is_exception(effect):
-> 1134 raise effect
1135 elif not _callable(effect):
1136 result = next(effect)
ValueError: 에러입니다
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-17-8718a2e60748> in <module>
1 # 한 번만 호출되었음을 확인
2 # 아직 한번도 호출되지 않았음을 확인
----> 3 mock.assert_called_once()
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py in assert_called_once(self)
884 self.call_count,
885 self._calls_repr()))
--> 886 raise AssertionError(msg)
887
888 def assert_called_with(self, /, *args, **kwargs):
AssertionError: Expected 'mock' to have been called once. Called 0 times.
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-24-cd640054a194> in <module>
1 # 호출되었으므로 에러 발생
----> 2 mock.assert_not_called()
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py in assert_not_called(self)
866 self.call_count,
867 self._calls_repr()))
--> 868 raise AssertionError(msg)
869
870 def assert_called(self):
AssertionError: Expected 'mock' to not have been called. Called 1 times.
Calls: [call(1, a=2)].
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-27-95c20c5fceb1> in <module>
----> 1 mock.assert_called_once_with(1, a=3)
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py in assert_called_once_with(self, *args, **kwargs)
917 self._calls_repr()))
918 raise AssertionError(msg)
--> 919 return self.assert_called_with(*args, **kwargs)
920
921
/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py in assert_called_with(self, *args, **kwargs)
905 if expected != actual:
906 cause = expected if isinstance(expected, Exception) else None
--> 907 raise AssertionError(_error_message()) from cause
908
909
AssertionError: expected call not found.
Expected: mock(1, a=3)
Actual: mock(1, a=2)
Out[29]:
[PosixPath('tests/data/oW63DwAAQBAJ_smallThumbnail.jpeg'),
PosixPath('tests/data/oW63DwAAQBAJ_thumbnail.jpeg')]
(주:tests/test_core.py)
from unittest.mock import patch
(생략)
class SaveThumbnailsTest(unittest.TestCase):
(생략)
# 테스트 대상의 save_thumbnail()가 이용할 참조 이름을 지정
@patch('booksearch.core.get_data')
def test_save_thumbnails(self, mock_get_data):
from booksearch.core import Book
# 앞에서 얻은 섬네일 이미지 데이터를 모의 객치의 반환값으로 설정
data_path = pathlib.Path(__file__).with_name('data')
mock_get_data.return_value = (
data_path / 'YkGmfbil6L4C_thumbnail.jpeg').read_bytes()
book = Book({'id': '', 'volumeInfo': {
'imageLinks': {
'thumbnail': THUMBNAIL_URL
}}})
filename = book.save_thumbnails(self.tmp.name)[0]
# get_data() 호출 시의 인수 확인
mock_get_data.assert_called_with(THUMBNAIL_URL)
# 저장된 데이터 확인
self.assertEqual(data, filename.read_bytes())$ python3 -m unittest -v
test_build_url (test_api.BuildUrlTest) ... ok
test_build_url_empty_param (test_api.BuildUrlTest) ... ok
test_build_url_fail (test_api.BuildUrlTest) ... expected failure
test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.040s
OK (expected failures=1)
(주:tests/test_api.py)
import sys
(생략)
class BuildUrlTest(unittest.TestCase):
(생략)
# 인수로 테스트를 건너 뛰는 이유를 전달
@unittest.skip('this is a skip test')
def test_nothing_skip(self):
pass
# 실행 중인 파이썬 버전이 3.6보다 높으면 건너뜀
@unittest.skipIf(sys.version_info > (3, 6),
'this is a skipIf test')
def test_nothing_skipIf(self):
pass$ python3 -m unittest -v
test_build_url (test_api.BuildUrlTest) ... ok
test_build_url_empty_param (test_api.BuildUrlTest) ... ok
test_build_url_fail (test_api.BuildUrlTest) ... expected failure
test_nothing_skip (test_api.BuildUrlTest) ... skipped 'this is a skip test'
test_nothing_skipIf (test_api.BuildUrlTest) ... skipped 'this is a skipIf test'
test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.038s
OK (skipped=2, expected failures=1)
(주:tests/test_core.py)
from urllib.error import URLError
(생략)
class GetBooksTest(unittest.TestCase):
def test_get_books_no_connection(self):
from booksearch.core import get_books
# 임시로 네트워크 접속 단절
with patch('socket.socket.connect') as mock:
# connect()가 호출되면 정확하지 않은 값을 반환함
mock.return_value = None
with self.assertRaisesRegex(URLError, 'urlopen error'):
# 예외가 발생하는 처리를 with 블록 안에서 실행
get_books(q='python')
test_get_books_no_connection (tests.test_core.GetBooksTest) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.086s
OK
(주:tests/test_api.py)
(생략)
class BuildUrlMultiTest(unittest.TestCase):
def test_build_url_multi(self):
from booksearch.api import build_url
base = 'https://www.googleapis.com/books/v1/volumes?'
expected_url = f'{base}q=python'
# 2번째, 3번째 테스트는 실패함
params = (
(expected_url, {'q': 'python'}),
(expected_url, {'q': 'python', 'maxResults': 1}),
(expected_url, {'q': 'python', 'langRestrict': 'en'}),
)
for expected, param in params:
with self.subTest(**param):
actual = build_url(param)
self.assertEqual(expected, actual)
======================================================================
FAIL: test_build_url_multi (tests.test_api.BuildUrlMultiTest) (q='python', maxResults=1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/suyamar/github/python-practice-book/src/12-unittest/workspace/tests/test_api.py", line 56, in test_build_url_multi
self.assertEqual(expected, actual)
AssertionError: 'https://www.googleapis.com/books/v1/volumes?q=python' != 'https://www.googleapis.com/books/v1/volumes?q=python&maxResults=1'
- https://www.googleapis.com/books/v1/volumes?q=python
+ https://www.googleapis.com/books/v1/volumes?q=python&maxResults=1
? +++++++++++++
======================================================================
FAIL: test_build_url_multi (tests.test_api.BuildUrlMultiTest) (q='python', langRestrict='en')
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/suyamar/github/python-practice-book/src/12-unittest/workspace/tests/test_api.py", line 56, in test_build_url_multi
self.assertEqual(expected, actual)
AssertionError: 'https://www.googleapis.com/books/v1/volumes?q=python' != 'https://www.googleapis.com/books/v1/volumes?q=python&langRestrict=en'
- https://www.googleapis.com/books/v1/volumes?q=python
+ https://www.googleapis.com/books/v1/volumes?q=python&langRestrict=en
? ++++++++++++++++
----------------------------------------------------------------------
Ran 1 test in 0.084s
FAILED (failures=2)
(주:tests/test_api.py)
import json
from io import StringIO
from unittest.mock import patch, MagicMock
(생략)
class GetJsonTest(unittest.TestCase):
def test_get_json(self):
from booksearch.api import get_json
with patch('booksearch.api.request.urlopen') as mock_urlopen:
# 컨텍스트 관리자의 목(mock)을 준비
# API 응답이 될 JSON 데이터 준비
expected_response = {'id': 'test'}
fp = StringIO(json.dumps(expected_response))
# MagicMock 클래스를 사용하면 __exit__를 추가할 필요 없음
mock = MagicMock()
mock.__enter__.return_value = fp
# urlopen()의 반환값이 컨텍스트 관리자
mock_urlopen.return_value = mock
actual = get_json({'q': 'python'})
self.assertEqual(expected_response, actual)
test_get_json (tests.test_api.GetJsonTest) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.057s
OK