{ "cells": [ { "cell_type": "raw", "metadata": {}, "source": [ "샘플 코드는 최종 파일만 배포합니다" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 단위 테스트 도입" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 단일 모듈 테스트" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:booksearch_module.py)\n", "import unittest\n", "\n", "\n", "# 애플리케이션 코드\n", "def booksearch():\n", " # 임의 처리\n", " return {}\n", "\n", "\n", "class BookSearchTest(unittest.TestCase):\n", " # booksearch() 테스트 코드\n", " def test_booksearch(self):\n", " self.assertEqual({}, booksearch())" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ ".\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.000s\n", "\n", "OK\n" ] } ], "source": [ "!python3 -m unittest booksearch_module.py" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "test_booksearch (booksearch_module.BookSearchTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.000s\n", "\n", "OK\n" ] } ], "source": [ "# -v 옵션으로 상세 정보 표시\n", "!python3 -m unittest -v booksearch_module.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 테스트 실행 명령어 간략화" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "import unittest\n", "\n", "\n", "# 애플리케이션 코드\n", "def booksearch():\n", " # 임의 처리\n", " return {}\n", "\n", "\n", "class BookSearchTest(unittest.TestCase):\n", " # booksearch() 테스트 코드\n", " def test_booksearch(self):\n", " self.assertEqual({}, booksearch())\n", "\n", "\n", "if __name__ == '__main__':\n", " unittest.main()\n" ] } ], "source": [ "!cat booksearch_module.py" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "test_booksearch (__main__.BookSearchTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.000s\n", "\n", "OK\n" ] } ], "source": [ "!python3 booksearch_module.py -v" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 패키지 테스트" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 디렉터리 구성" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/Users/yeonsookim/Workspaces/personal/python-src/12-unittest/workspace\n" ] } ], "source": [ "# booksearch/, tests/를 준비한 디렉터리로 이동합니다\n", "%cd workspace" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "$ python3 -m unittest\n", "\n", "----------------------------------------------------------------------\n", "Ran 0 tests in 0.000s\n", "\n", "OK" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 샘플 애플리케이션 작성" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "from .core import Book, get_books\n", "__all__ = ['Book', 'get_books']" ] } ], "source": [ "!cat booksearch/__init__.py" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "import json\n", "from urllib import request, parse\n", "\n", "def get_json(param):\n", " with request.urlopen(build_url(param)) as f:\n", " return json.load(f)\n", "\n", "def get_data(url):\n", " with request.urlopen(url) as f:\n", " return f.read()\n", "\n", "def build_url(param):\n", " query = parse.urlencode(param)\n", " return ('https://www.googleapis.com'\n", " f'/books/v1/volumes?{query}')" ] } ], "source": [ "!cat booksearch/api.py" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "import imghdr\n", "import pathlib\n", "from .api import get_data, get_json\n", "\n", "\n", "class Book:\n", " \"\"\"API 응답의 VolumeInfo 엘리먼트에 대응\"\"\"\n", "\n", " def __init__(self, item):\n", " self.id = item['id']\n", " volume_info = item['volumeInfo']\n", " for k, v in volume_info.items():\n", " setattr(self, str(k), v)\n", "\n", " def __repr__(self):\n", " return str(self.__dict__)\n", "\n", " def save_thumbnails(self, prefix):\n", " \"\"\"섬네일 이미지를 저장함\"\"\"\n", " paths = []\n", " for kind, url in self.imageLinks.items():\n", " thumbnail = get_data(url)\n", " # 이미지 데이터로부터 확장자 판정\n", " ext = imghdr.what(None, h=thumbnail)\n", " # pathlib.Path는 / 연산자로 경로를 추가할 수 있음\n", " base = pathlib.Path(\n", " prefix) / f'{self.id}_{kind}'\n", " filename = base.with_suffix(f'.{ext}')\n", " filename.write_bytes(thumbnail)\n", " paths.append(filename)\n", " return paths\n", "\n", "\n", "def get_books(q, **params):\n", " \"\"\"서적 검색 수행\"\"\"\n", " params['q'] = q\n", " data = get_json(params)\n", " return [Book(item) for item in data['items']]\n" ] } ], "source": [ "!cat booksearch/core.py" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "ename": "URLError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mSSLCertVerificationError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py\u001b[0m in \u001b[0;36mdo_open\u001b[0;34m(self, http_class, req, **http_conn_args)\u001b[0m\n\u001b[1;32m 1318\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1319\u001b[0;31m h.request(req.get_method(), req.selector, req.data, headers,\n\u001b[0m\u001b[1;32m 1320\u001b[0m encode_chunked=req.has_header('Transfer-encoding'))\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py\u001b[0m in \u001b[0;36mrequest\u001b[0;34m(self, method, url, body, headers, encode_chunked)\u001b[0m\n\u001b[1;32m 1229\u001b[0m \u001b[0;34m\"\"\"Send a complete request to the server.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1230\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_send_request\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmethod\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbody\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mheaders\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mencode_chunked\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1231\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py\u001b[0m in \u001b[0;36m_send_request\u001b[0;34m(self, method, url, body, headers, encode_chunked)\u001b[0m\n\u001b[1;32m 1275\u001b[0m \u001b[0mbody\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_encode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbody\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'body'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1276\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mendheaders\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbody\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mencode_chunked\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mencode_chunked\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1277\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py\u001b[0m in \u001b[0;36mendheaders\u001b[0;34m(self, message_body, encode_chunked)\u001b[0m\n\u001b[1;32m 1224\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mCannotSendHeader\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1225\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_send_output\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmessage_body\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mencode_chunked\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mencode_chunked\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1226\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py\u001b[0m in \u001b[0;36m_send_output\u001b[0;34m(self, message_body, encode_chunked)\u001b[0m\n\u001b[1;32m 1003\u001b[0m \u001b[0;32mdel\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_buffer\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1004\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1005\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py\u001b[0m in \u001b[0;36msend\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 943\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mauto_open\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 944\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconnect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 945\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/http/client.py\u001b[0m in \u001b[0;36mconnect\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1398\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1399\u001b[0;31m self.sock = self._context.wrap_socket(self.sock,\n\u001b[0m\u001b[1;32m 1400\u001b[0m server_hostname=server_hostname)\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ssl.py\u001b[0m in \u001b[0;36mwrap_socket\u001b[0;34m(self, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, session)\u001b[0m\n\u001b[1;32m 499\u001b[0m \u001b[0;31m# ctx._wrap_socket()\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 500\u001b[0;31m return self.sslsocket_class._create(\n\u001b[0m\u001b[1;32m 501\u001b[0m \u001b[0msock\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msock\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ssl.py\u001b[0m in \u001b[0;36m_create\u001b[0;34m(cls, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, context, session)\u001b[0m\n\u001b[1;32m 1039\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"do_handshake_on_connect should not be specified for non-blocking sockets\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1040\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdo_handshake\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1041\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mOSError\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ssl.py\u001b[0m in \u001b[0;36mdo_handshake\u001b[0;34m(self, block)\u001b[0m\n\u001b[1;32m 1308\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msettimeout\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1309\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_sslobj\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdo_handshake\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1310\u001b[0m \u001b[0;32mfinally\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mSSLCertVerificationError\u001b[0m: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1108)", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[0;31mURLError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mbooksearch\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mget_books\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mbooks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_books\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mq\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'python'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m~/Workspaces/personal/python-src/12-unittest/workspace/booksearch/core.py\u001b[0m in \u001b[0;36mget_books\u001b[0;34m(q, **params)\u001b[0m\n\u001b[1;32m 35\u001b[0m \u001b[0;34m\"\"\"서적 검색 수행\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 36\u001b[0m \u001b[0mparams\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'q'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mq\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 37\u001b[0;31m \u001b[0mdata\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_json\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 38\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mBook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mitem\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'items'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/Workspaces/personal/python-src/12-unittest/workspace/booksearch/api.py\u001b[0m in \u001b[0;36mget_json\u001b[0;34m(param)\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_json\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparam\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0;32mwith\u001b[0m \u001b[0mrequest\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0murlopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbuild_url\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparam\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mjson\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mload\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mf\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py\u001b[0m in \u001b[0;36murlopen\u001b[0;34m(url, data, timeout, cafile, capath, cadefault, context)\u001b[0m\n\u001b[1;32m 220\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 221\u001b[0m \u001b[0mopener\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_opener\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 222\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mopener\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 223\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 224\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0minstall_opener\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mopener\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py\u001b[0m in \u001b[0;36mopen\u001b[0;34m(self, fullurl, data, timeout)\u001b[0m\n\u001b[1;32m 523\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 524\u001b[0m \u001b[0msys\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maudit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'urllib.Request'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfull_url\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mheaders\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_method\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 525\u001b[0;31m \u001b[0mresponse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_open\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mreq\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 526\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 527\u001b[0m \u001b[0;31m# post-process response\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py\u001b[0m in \u001b[0;36m_open\u001b[0;34m(self, req, data)\u001b[0m\n\u001b[1;32m 540\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 541\u001b[0m \u001b[0mprotocol\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mreq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtype\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 542\u001b[0;31m result = self._call_chain(self.handle_open, protocol, protocol +\n\u001b[0m\u001b[1;32m 543\u001b[0m '_open', req)\n\u001b[1;32m 544\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py\u001b[0m in \u001b[0;36m_call_chain\u001b[0;34m(self, chain, kind, meth_name, *args)\u001b[0m\n\u001b[1;32m 500\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhandler\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mhandlers\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 501\u001b[0m \u001b[0mfunc\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mgetattr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhandler\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmeth_name\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 502\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 503\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 504\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py\u001b[0m in \u001b[0;36mhttps_open\u001b[0;34m(self, req)\u001b[0m\n\u001b[1;32m 1360\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1361\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mhttps_open\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreq\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1362\u001b[0;31m return self.do_open(http.client.HTTPSConnection, req,\n\u001b[0m\u001b[1;32m 1363\u001b[0m context=self._context, check_hostname=self._check_hostname)\n\u001b[1;32m 1364\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/urllib/request.py\u001b[0m in \u001b[0;36mdo_open\u001b[0;34m(self, http_class, req, **http_conn_args)\u001b[0m\n\u001b[1;32m 1320\u001b[0m encode_chunked=req.has_header('Transfer-encoding'))\n\u001b[1;32m 1321\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mOSError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0merr\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# timeout error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1322\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mURLError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0merr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1323\u001b[0m \u001b[0mr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mh\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgetresponse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1324\u001b[0m \u001b[0;32mexcept\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mURLError\u001b[0m: " ] } ], "source": [ "from booksearch import get_books\n", "books = get_books(q='python')" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'books' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# 실행 시 얻은 데이터에 따라 결과가 달라짐\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mbooks\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mNameError\u001b[0m: name 'books' is not defined" ] } ], "source": [ "# 실행 시 얻은 데이터에 따라 결과가 달라짐\n", "books[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# unittest 모듈 ── 표준 단위 테스트 라이브러리" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 테스트 케이스 구현" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "import unittest\n", "\n", "class BuildUrlTest(unittest.TestCase):\n", " def test_build_url(self):\n", " # build_url()이 테스트 대상 처리\n", " from booksearch.api import build_url\n", " expected = 'https://www.googleapis.com/books/v1/volumes?q=python'\n", " actual = build_url({'q': 'python'})\n", " # assert 메서드 이용\n", " self.assertEqual(expected, actual)" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "(생략)\n", " actual = build_url({'q': 'python'})\n", " # assert 메서드 이용\n", " self.assertEqual(expected, actual)\n", "\n", " # 좋지 않은 예시\n", " # 위 행이 실패하면 아래 행이 실행되지 않음\n", " expected2 = 'https://www.googleapis.com/books/v1/volumes?'\n", " actual2 = build_url({})\n", " self.assertEqual(expected2, actual2)" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "(생략)\n", " actual = build_url({'q': 'python'})\n", " # assert 메서드 이용\n", " self.assertEqual(expected, actual)\n", "\n", " # 좋은 예\n", " def test_build_url_empty_param(self):\n", " from booksearch.api import build_url\n", " expected = 'https://www.googleapis.com/books/v1/volumes?'\n", " actual = build_url({})\n", " self.assertEqual(expected, actual)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 전처리, 후처리가 필요한 테스트 케이스" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_core.py)\n", "import pathlib\n", "import unittest\n", "from tempfile import TemporaryDirectory\n", "\n", "THUMBNAIL_URL = (\n", " 'http://books.google.com/books/content'\n", " '?id=OgtBw76OY5EC&printsec=frontcover'\n", " '&img=1&zoom=1&edge=curl&source=gbs_api'\n", ")\n", "\n", "class SaveThumbnailsTest(unittest.TestCase):\n", " def setUp(self):\n", " # 임시 디렉터리 작성\n", " self.tmp = TemporaryDirectory()\n", "\n", " def tearDown(self):\n", " # 임시 디렉터리 정리\n", " self.tmp.cleanup()\n", "\n", " def test_save_thumbnails(self):\n", " from booksearch.core import Book\n", " book = Book({'id': '', 'volumeInfo': {\n", " 'imageLinks': {\n", " 'thumbnail': THUMBNAIL_URL\n", " }}})\n", " # 처리를 실행하고 파일이 작성되었음을 확인\n", " filename = book.save_thumbnails(self.tmp.name)[0]\n", " self.assertTrue(pathlib.Path(filename).exists())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 테스트 실행과 결과 확인" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "$ python3 -m unittest\n", "...\n", "----------------------------------------------------------------------\n", "Ran 3 tests in 0.899s\n", "\n", "OK" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "$ python3 -m unittest -v\n", "test_build_url (test_api.BuildUrlTest) ... ok\n", "test_build_url_empty_param (test_api.BuildUrlTest) ... ok\n", "test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 3 tests in 0.337s\n", "\n", "OK" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 테스트 실패 시 결과" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "class BuildUrlTest(unittest.TestCase):\n", " (생략)\n", " def test_build_url_fail(self):\n", " from booksearch.api import build_url\n", " expected = 'https://www.googleapis.com/books/v1/volumes'\n", " actual = build_url({})\n", " self.assertEqual(expected, actual,\n", " msg='이 테스트는 실패합니다')" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "$ python3 -m unittest -v\n", "test_build_url (test_api.BuildUrlTest) ... ok\n", "test_build_url_empty_param (test_api.BuildUrlTest) ... ok\n", "test_build_url_fail (test_api.BuildUrlTest) ... FAIL\n", "test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok\n", "\n", "======================================================================\n", "FAIL: test_build_url_fail (test_api.BuildUrlTest)\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"/Users/.../workspace/tests/test_api.py\", line 24, in test_build_url_fail\n", " msg='이 테스트는 실패합니다')\n", "AssertionError: 'https://www.googleapis.com/books/v1/volumes' != 'https://www.googleapis.com/books/v1/volumes?'\n", "- https://www.googleapis.com/books/v1/volumes\n", "+ https://www.googleapis.com/books/v1/volumes?\n", "? +\n", " : 이 테스트는 실패합니다\n", "\n", "----------------------------------------------------------------------\n", "Ran 4 tests in 0.397s\n", "\n", "FAILED (failures=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 테스트 실패 시 결과 억제하기" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "class BuildUrlTest(unittest.TestCase):\n", " (생략)\n", " @unittest.expectedFailure\n", " def test_build_url_fail(self):" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "$ python3 -m unittest -v\n", "test_build_url (test_api.BuildUrlTest) ... ok\n", "test_build_url_empty_param (test_api.BuildUrlTest) ... ok\n", "test_build_url_fail (test_api.BuildUrlTest) ... expected failure\n", "test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 4 tests in 0.400s\n", "\n", "OK (expected failures=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 특정 테스트만 실행하기" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 테스트 케이스 직접 실행" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "# 모듈 지정\n", "$ python3 -m unittest -v tests.test_api\n", "test_build_url (tests.test_api.BuildUrlTest) ... ok\n", "test_build_url_empty_param (tests.test_api.BuildUrlTest) ... ok\n", "test_build_url_fail (tests.test_api.BuildUrlTest) ... expected failure\n", "\n", "----------------------------------------------------------------------\n", "Ran 3 tests in 0.049s\n", "\n", "OK (expected failures=1)\n", "\n", "# 클래스 지정\n", "$ python3 -m unittest -v tests.test_api.BuildUrlTest\n", "(생략)\n", "\n", "# 메서드 지정\n", "$ python3 -m unittest -v tests.test_api.BuildUrlTest.test_build_url\n", "(생략)\n", "\n", "# 여러 테스트 지정\n", "$ python3 -m unittest -v tests.test_api tests.test_core\n", "(생략)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 테스트 디스커버리" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "# 실행할 테스트 모듈 이름을 지정\n", "$ python3 -m unittest discover -s tests -p 'test_c*.py' -v\n", "test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.362s\n", "\n", "OK" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# unittest.mock 모듈 ── 목(모의) 객체 이용" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 목 객체 기본 사용법" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 임의의 값을 반환하는 호출 가능 객체로서 이용" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from unittest.mock import Mock\n", "\n", "# 인수에 반환값을 설정\n", "mock = Mock(return_value=3)\n", "mock()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "4" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# return_value는 나중에도 설정할 수 있음\n", "mock.return_value=4\n", "\n", "# 호출 시 인수는 반환값에 영향 없음\n", "mock(1)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 함수에서는 인수가 그대로 전달됨\n", "mock = Mock(side_effect=lambda x: x % 2)\n", "mock(3)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# side_effect는 나중에도 설정할 수 있음\n", "# 이터러블을 사용하면 호출할 때마다 앞에서부터 순서대로 반환됨\n", "mock.side_effect=[2, 1]\n", "mock()" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mock()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "ValueError", "evalue": "에러입니다", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# 예외 클래스나 그 인스턴스에서는 해당 예외가 전송됨\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mmock\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mside_effect\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'에러입니다'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mmock\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1073\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_mock_check_sig\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1074\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_increment_mock_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1075\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_mock_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1076\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1077\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py\u001b[0m in \u001b[0;36m_mock_call\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1077\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1078\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_mock_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m/\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1079\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_execute_mock_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1080\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1081\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_increment_mock_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m/\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py\u001b[0m in \u001b[0;36m_execute_mock_call\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1132\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0meffect\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1133\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0m_is_exception\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1134\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0meffect\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1135\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0m_callable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1136\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0meffect\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mValueError\u001b[0m: 에러입니다" ] } ], "source": [ "# 예외 클래스나 그 인스턴스에서는 해당 예외가 전송됨\n", "mock.side_effect = ValueError('에러입니다')\n", "mock()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Assert 메서드로 호출 여부 테스트" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "mock = Mock(return_value=3)\n", "\n", "# 아직 한 번도 호출되지 않았음을 확인\n", "mock.assert_not_called()" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "AssertionError", "evalue": "Expected 'mock' to have been called once. Called 0 times.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# 한 번만 호출되었음을 확인\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;31m# 아직 한번도 호출되지 않았음을 확인\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mmock\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0massert_called_once\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py\u001b[0m in \u001b[0;36massert_called_once\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 884\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcall_count\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 885\u001b[0m self._calls_repr()))\n\u001b[0;32m--> 886\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mAssertionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 887\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 888\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0massert_called_with\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m/\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: Expected 'mock' to have been called once. Called 0 times." ] } ], "source": [ "# 한 번만 호출되었음을 확인\n", "# 아직 한 번도 호출되지 않았으므로 에러가 발생함\n", "mock.assert_called_once()" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 호출해 봄\n", "mock(1, a=2)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "AssertionError", "evalue": "Expected 'mock' to not have been called. Called 1 times.\nCalls: [call(1, a=2)].", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# 호출되었으므로 에러 발생\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mmock\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0massert_not_called\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py\u001b[0m in \u001b[0;36massert_not_called\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 866\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcall_count\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 867\u001b[0m self._calls_repr()))\n\u001b[0;32m--> 868\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mAssertionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 869\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 870\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0massert_called\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: Expected 'mock' to not have been called. Called 1 times.\nCalls: [call(1, a=2)]." ] } ], "source": [ "# 호출되었으므로 에러 발생\n", "mock.assert_not_called()" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "# 한 번만 호출된 것을 확인\n", "mock.assert_called_once()" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "# 호출 여부를 확인\n", "mock.assert_called_once_with(1, a=2)" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "tags": [ "raises-exception" ] }, "outputs": [ { "ename": "AssertionError", "evalue": "expected call not found.\nExpected: mock(1, a=3)\nActual: mock(1, a=2)", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mmock\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0massert_called_once_with\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py\u001b[0m in \u001b[0;36massert_called_once_with\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 917\u001b[0m self._calls_repr()))\n\u001b[1;32m 918\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mAssertionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 919\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0massert_called_with\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 920\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 921\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/unittest/mock.py\u001b[0m in \u001b[0;36massert_called_with\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 905\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mexpected\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mactual\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 906\u001b[0m \u001b[0mcause\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mexpected\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexpected\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 907\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mAssertionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_error_message\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mcause\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 908\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 909\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: expected call not found.\nExpected: mock(1, a=3)\nActual: mock(1, a=2)" ] } ], "source": [ "mock.assert_called_once_with(1, a=3)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "# 호출된 횟수는 확인하지 못하며, 일부 인수만 확인\n", "from unittest.mock import ANY\n", "mock.assert_called_with(1, a=ANY)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## patch를 사용한 객체 치환" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[]\n" ] } ], "source": [ "from booksearch import get_books\n", "from unittest.mock import patch\n", "\n", "# 대화형 모드에서는 __main__ 모듈에서 이름을 지정\n", "with patch('__main__.get_books') as mock_get_books:\n", " mock_get_books.return_value = []\n", " print(get_books())" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "@patch('__main__.get_books')\n", "def test_use_mock(mock_get_books):\n", " mock_get_books.return_value = []\n", " return get_books()" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "test_use_mock()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## mock을 이용한 테스트 케이스 실제 사례" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "from booksearch import get_books\n", "book = get_books(q='python')[0]" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[PosixPath('tests/data/oW63DwAAQBAJ_smallThumbnail.jpeg'),\n", " PosixPath('tests/data/oW63DwAAQBAJ_thumbnail.jpeg')]" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 실행 시에 얻은 데이터에 따라 결과는 다름\n", "book.save_thumbnails('tests/data')" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_core.py)\n", "from unittest.mock import patch\n", "(생략)\n", "class SaveThumbnailsTest(unittest.TestCase):\n", " (생략)\n", " # 테스트 대상의 save_thumbnail()가 이용할 참조 이름을 지정\n", " @patch('booksearch.core.get_data')\n", " def test_save_thumbnails(self, mock_get_data):\n", " from booksearch.core import Book\n", "\n", " # 앞에서 얻은 섬네일 이미지 데이터를 모의 객치의 반환값으로 설정\n", " data_path = pathlib.Path(__file__).with_name('data')\n", " mock_get_data.return_value = (\n", " data_path / 'YkGmfbil6L4C_thumbnail.jpeg').read_bytes()\n", "\n", " book = Book({'id': '', 'volumeInfo': {\n", " 'imageLinks': {\n", " 'thumbnail': THUMBNAIL_URL\n", " }}})\n", " filename = book.save_thumbnails(self.tmp.name)[0]\n", "\n", " # get_data() 호출 시의 인수 확인\n", " mock_get_data.assert_called_with(THUMBNAIL_URL)\n", "\n", " # 저장된 데이터 확인\n", " self.assertEqual(data, filename.read_bytes())" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "$ python3 -m unittest -v\n", "test_build_url (test_api.BuildUrlTest) ... ok\n", "test_build_url_empty_param (test_api.BuildUrlTest) ... ok\n", "test_build_url_fail (test_api.BuildUrlTest) ... expected failure\n", "test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 4 tests in 0.040s\n", "\n", "OK (expected failures=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 유스 케이스 별 테스트 케이스 구현" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 환경 의존 테스트 건너 뛰기" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "import sys\n", "(생략)\n", "class BuildUrlTest(unittest.TestCase):\n", " (생략)\n", " # 인수로 테스트를 건너 뛰는 이유를 전달\n", " @unittest.skip('this is a skip test')\n", " def test_nothing_skip(self):\n", " pass\n", "\n", " # 실행 중인 파이썬 버전이 3.6보다 높으면 건너뜀\n", " @unittest.skipIf(sys.version_info > (3, 6),\n", " 'this is a skipIf test')\n", " def test_nothing_skipIf(self):\n", " pass" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "$ python3 -m unittest -v\n", "test_build_url (test_api.BuildUrlTest) ... ok\n", "test_build_url_empty_param (test_api.BuildUrlTest) ... ok\n", "test_build_url_fail (test_api.BuildUrlTest) ... expected failure\n", "test_nothing_skip (test_api.BuildUrlTest) ... skipped 'this is a skip test'\n", "test_nothing_skipIf (test_api.BuildUrlTest) ... skipped 'this is a skipIf test'\n", "test_save_thumbnails (test_core.SaveThumbnailsTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 6 tests in 0.038s\n", "\n", "OK (skipped=2, expected failures=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 예외 발생 테스트" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_core.py)\n", "from urllib.error import URLError\n", "(생략)\n", "class GetBooksTest(unittest.TestCase):\n", " def test_get_books_no_connection(self):\n", " from booksearch.core import get_books\n", "\n", " # 임시로 네트워크 접속 단절\n", " with patch('socket.socket.connect') as mock:\n", " # connect()가 호출되면 정확하지 않은 값을 반환함\n", " mock.return_value = None\n", " with self.assertRaisesRegex(URLError, 'urlopen error'):\n", " # 예외가 발생하는 처리를 with 블록 안에서 실행\n", " get_books(q='python')" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "test_get_books_no_connection (tests.test_core.GetBooksTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.086s\n", "\n", "OK\n" ] } ], "source": [ "!python3 -m unittest tests.test_core.GetBooksTest -v" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 다른 파라미터로 동일한 테스트 반복하기" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "(생략)\n", "class BuildUrlMultiTest(unittest.TestCase):\n", " def test_build_url_multi(self):\n", " from booksearch.api import build_url\n", " base = 'https://www.googleapis.com/books/v1/volumes?'\n", " expected_url = f'{base}q=python'\n", " # 2번째, 3번째 테스트는 실패함\n", " params = (\n", " (expected_url, {'q': 'python'}),\n", " (expected_url, {'q': 'python', 'maxResults': 1}),\n", " (expected_url, {'q': 'python', 'langRestrict': 'en'}),\n", " )\n", " for expected, param in params:\n", " with self.subTest(**param):\n", " actual = build_url(param)\n", " self.assertEqual(expected, actual)" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "======================================================================\n", "FAIL: test_build_url_multi (tests.test_api.BuildUrlMultiTest) (q='python', maxResults=1)\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"/Users/suyamar/github/python-practice-book/src/12-unittest/workspace/tests/test_api.py\", line 56, in test_build_url_multi\n", " self.assertEqual(expected, actual)\n", "AssertionError: 'https://www.googleapis.com/books/v1/volumes?q=python' != 'https://www.googleapis.com/books/v1/volumes?q=python&maxResults=1'\n", "- https://www.googleapis.com/books/v1/volumes?q=python\n", "+ https://www.googleapis.com/books/v1/volumes?q=python&maxResults=1\n", "? +++++++++++++\n", "\n", "\n", "======================================================================\n", "FAIL: test_build_url_multi (tests.test_api.BuildUrlMultiTest) (q='python', langRestrict='en')\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"/Users/suyamar/github/python-practice-book/src/12-unittest/workspace/tests/test_api.py\", line 56, in test_build_url_multi\n", " self.assertEqual(expected, actual)\n", "AssertionError: 'https://www.googleapis.com/books/v1/volumes?q=python' != 'https://www.googleapis.com/books/v1/volumes?q=python&langRestrict=en'\n", "- https://www.googleapis.com/books/v1/volumes?q=python\n", "+ https://www.googleapis.com/books/v1/volumes?q=python&langRestrict=en\n", "? ++++++++++++++++\n", "\n", "\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.084s\n", "\n", "FAILED (failures=2)\n" ] } ], "source": [ "!python3 -m unittest tests.test_api.BuildUrlMultiTest" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 컨텍스트 관리자 테스트하기" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "(주:tests/test_api.py)\n", "import json\n", "from io import StringIO\n", "from unittest.mock import patch, MagicMock\n", "(생략)\n", "class GetJsonTest(unittest.TestCase):\n", " def test_get_json(self):\n", " from booksearch.api import get_json\n", " with patch('booksearch.api.request.urlopen') as mock_urlopen:\n", " # 컨텍스트 관리자의 목(mock)을 준비\n", " # API 응답이 될 JSON 데이터 준비\n", " expected_response = {'id': 'test'}\n", " fp = StringIO(json.dumps(expected_response))\n", "\n", " # MagicMock 클래스를 사용하면 __exit__를 추가할 필요 없음\n", " mock = MagicMock()\n", " mock.__enter__.return_value = fp\n", " # urlopen()의 반환값이 컨텍스트 관리자\n", " mock_urlopen.return_value = mock\n", " actual = get_json({'q': 'python'})\n", " self.assertEqual(expected_response, actual)" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "test_get_json (tests.test_api.GetJsonTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.057s\n", "\n", "OK\n" ] } ], "source": [ "!python3 -m unittest tests.test_api.GetJsonTest -v" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 정리" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.7" } }, "nbformat": 4, "nbformat_minor": 4 }