{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "818629e5",
   "metadata": {},
   "source": [
    "# Тестирование #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc9a38b8",
   "metadata": {},
   "source": [
    "Привет. Настало время поговорить о тестировании, которого так бояться многие программисты.\n",
    "\n",
    "Допустим вы написали программу. Как же проверить, что она работает корректно? Вы можете подать ей на вход какие-то данные и посмотреть, что получается на выходе, действительно ли это то, чего вы ожидаете. Однако, допустим вы изменили вашу программу или что, если вы работаете над большим проектом, с большим количеством разработчиков и туда постоянно вносятся изменения. Вам нужно постоянно проверять правильно ли работает ваша программа в различных условиях. Именно это и называется тестированием. На самом деле тестированию можно посвятить отдельную тему, отдельный курс, отдельную специализацию, потому что это огромная область.\n",
    "\n",
    "Мы с вами разберем наиболее популярный, наиболее распространенный вид тестирования - это `unit` тестирование."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d02bc71c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# test_python.py\n",
    "\n",
    "import unittest\n",
    "\n",
    "class TestPython(unittest.TestCase):\n",
    "    def test_float_to_int_coercion(self):\n",
    "        self.assertEqual(1, int(1.0))\n",
    "        \n",
    "    def test_get_empty_dict(self):\n",
    "        self.assertIsNone({}.get(\"key\"))\n",
    "        \n",
    "    def test_trueness(self):\n",
    "        self.assertTrue(bool(10))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "992814e4",
   "metadata": {},
   "source": [
    "`Unit` тесты призваны протестировать какую-то небольшую функциональность, функцию, класс или модуль, посмотреть корректно ли он работает. Вы можете написать `unit` тесты к вашем классу, чтобы проверять все ли он делает корректно. Чтобы определить свой `unittest` можно воспользоваться стандартной библиотекой модулей `unittest` и определить свой класс, который наследуется от `TestCase`'а из модуля `unittest`. Дальше вы можете определить функции, которые, собственно, и будут являться тестами. Каждая функция, которая начинается с `test` и нижнего подчеркивания, является тестом и внутри этого теста вы можете проверить какие-то условия. В данном случае мы можем проверить правильно ли у нас приводится тип в случае `int`'a и `float`'а, или, например, корректно ли у нас работает функция `get` у пустого словаря. Делается это с помощью методов `TestCase`'а. Их довольно много - есть `assertEqual`, `assertIsNone`, `assertRaises` и так далее. Вы можете посмотреть про это в документации. Все они делают одно - они проверяют корректно ли работает выражение, корректно ли вызывается функция и так далее.\n",
    "\n",
    "Чтобы запустить тесты можно воспользоваться консолью. Еще чаще тесты запускает какая-то автоматическая система сборки или тестирования, или, например, ваше IDE может запускать тесты. Давайте перейдем в консоль и давайте запустим наши тесты. В данном случае мы тестируем Python, проверяем корректно ли он работает. Давайте запустим тесты."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "4b1f75b5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "...\r\n",
      "----------------------------------------------------------------------\r\n",
      "Ran 3 tests in 0.000s\r\n",
      "\r\n",
      "OK\r\n"
     ]
    }
   ],
   "source": [
    "! python -m unittest test_python.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81c9b6ff",
   "metadata": {},
   "source": [
    "Наши тесты прошли. Об этом говорят три точки и надпись, что три теста прошло. Замечательно.\n",
    "\n",
    "Если б у нас какой-то тест упал, было бы что-то по-другому. Давайте посмотрим, например, когда у нас падает наш тест."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f3b615cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# test_division.py\n",
    "\n",
    "import unittest\n",
    "\n",
    "class TestDivision(unittest.TestCase):\n",
    "    def test_integer_division(self):\n",
    "        self.assertIs(10 / 5, 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2c03577",
   "metadata": {},
   "source": [
    "Мы определяем тест на деление, и вы можете заметить, что действительно скорее всего он упадет, потому что при делении двух целых чисел в Python'е 3 получается `float`, а не `int`. Давайте запустим тестирование деления."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c51994b7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "F\r\n",
      "======================================================================\r\n",
      "FAIL: test_integer_division (test_division.TestDivision)\r\n",
      "----------------------------------------------------------------------\r\n",
      "Traceback (most recent call last):\r\n",
      "  File \"/home/mikhaylovaf/projects/python/4. Углубленный Python/3. Отладка и тестирование/test_division.py\", line 5, in test_integer_division\r\n",
      "    self.assertIs(10 / 5, 2)\r\n",
      "AssertionError: 2.0 is not 2\r\n",
      "\r\n",
      "----------------------------------------------------------------------\r\n",
      "Ran 1 test in 0.001s\r\n",
      "\r\n",
      "FAILED (failures=1)\r\n"
     ]
    }
   ],
   "source": [
    "! python -m unittest test_division.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04307ce5",
   "metadata": {},
   "source": [
    "Да, наш тест упал. Об этом говорит буква `F` и описание, собственно, падения. Можем посмотреть, что у нас упала функция `test_integer_division`, и с каким `AssertionError`'ом конкретно она упала. Собственно, если упал тест, вы всегда можете посмотреть почему и исправить вашу функциональность."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7eec0ffa",
   "metadata": {},
   "source": [
    "Давайте посмотрим на конкретный пример и напишем свой собственный класс, который попробуем протестировать. Класс будет называться `Asteroid` и он призван помочь нам работать с открытым `api NASA` по астероидам и каким-то телам, которые летают вокруг Земли. Давайте определим наш класс и передадим туда уникальный идентификатор астероида, который мы хотим исследовать, данные о котором мы хотим узнать. Запишем соответствующий `api_url` и будем использовать функцию `get_data`, которая идет в Интернет в `api` и забирает информацию с сайта `NASA`. Информация в `json`'е, поэтому очень легко с этим работать. Мы воспользуемся библиотекой `requests` и просто будем возвращать из `get_data` какой-то словарь данных. Дальше мы можем определить различные методы, и в данном случае `property`, которые возвращают данные про наш астероид. В данном случае мы можем получить его имя или его размер в метрах с помощью функции `diameter`. Опять же вы можете заметить, что мы каждый раз вызываем функцию `get_data` и каждый раз идем в Интернет. Можно это оптимизировать и ходить в Интернет один раз, это действительно так. Давайте протестируем наш класс и посмотрим, корректно ли работает наша функция `name` и наша функция `diameter`. Однако, вы можете заметить, что здесь есть некоторая тонкость. Каждый раз, когда мы будем запускать тесты, у нас наш класс, `TestCase` будет ходить в Интернет, потому что у нас запускается функция `get_data`. Это не всегда будет работать, потому что мы можем запускать наши тесты, например, в окружении, в котором нет Интернета, или Интернет медленный, или мы экономим трафик. Точно то же самое можно сказать про работу с Сетью в принципе или про работу с какими-то другими ресурсами, например, с диском. Возможно мы не хотим загружать диск. Что же сделать в таком случае? На помощь нам придет несколько механизмов, о которых мы поговорим прямо сейчас."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "e78c44bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "class Asteroid:\n",
    "    BASE_API_URL = \"https://api.nasa.gov/neo/rest/v1/neo/{}?api_key=DEMO_KEY\"\n",
    "    \n",
    "    def __init__(self, spk_id):\n",
    "        self.api_url = self.BASE_API_URL.format(spk_id)\n",
    "        \n",
    "    def get_data(self):\n",
    "        return requests.get(self.api_url).json()\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self.get_data()[\"name\"]\n",
    "    \n",
    "    @property\n",
    "    def diameter(self):\n",
    "        return int(self.get_data()[\"estimated_diameter\"][\"meters\"][\"estimated_diameter_max\"])\n",
    "    \n",
    "    @property\n",
    "    def closest_approach(self):\n",
    "        closest = {\n",
    "            \"date\": None,\n",
    "            \"distance\": float(\"inf\")\n",
    "        }\n",
    "        \n",
    "        for approach in self.get_data()[\"close_approach_data\"]:\n",
    "            distance = float(approach[\"miss_distance\"][\"lunar\"])\n",
    "            if distance < closest[\"distance\"]:\n",
    "                closest.update({\n",
    "                    \"date\": approach[\"close_approach_date\"],\n",
    "                    \"distance\": distance\n",
    "                })\n",
    "                \n",
    "        return closest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f93f4f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "apophis = Asteroid(2099942)\n",
    "\n",
    "print(f\"Name: {apophis.name}\")\n",
    "print(f\"Diameter: {apophis.diameter}m\")\n",
    "\n",
    "print(f\"Date: {apophis.closest_approach['date']}\")\n",
    "print(f\"Distance: {apophis.closest_approach['distance']:.2} LD\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5fb55d0d",
   "metadata": {},
   "source": [
    "Итак, давайте напишем наш `TestCase` и тестировать мы будем астероид с таким вот `id`'шником. Это астероид `apophis`, про который было много шума совсем недавно. Довольно большой астероид, который довольно часто пролетает мимо Земли. Итак, напишем наш `TestCase` и познакомимся с некоторыми новыми моментами."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "3daa47dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import unittest\n",
    "from unittest.mock import patch\n",
    "\n",
    "from asteroid import Asteroid\n",
    "\n",
    "class TestAsteroid(unittest.TestCase):\n",
    "    def setUp(self):\n",
    "        self.asteroid = Asteroid(2099942)\n",
    "        \n",
    "    def mocked_get_data(self):\n",
    "        with open(\"apophis_fixture.txt\") as f:\n",
    "            return json.loads(f.read())\n",
    "        \n",
    "    @patch(\"asteroid.Asteroid.get_data\", mocked_get_data)\n",
    "    def test_name(self):\n",
    "        self.assertEqual(\n",
    "            self.asteroid.name, \"99942 Apophis (2004 MN4)\"\n",
    "        )\n",
    "        \n",
    "    @patch(\"asteroid.Asteroid.get_data\", mocked_get_data)\n",
    "    def test_diameter(self):\n",
    "        self.assertEqual(self.asteroid.diameter, 682)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2185e4b3",
   "metadata": {},
   "source": [
    "Итак, во-первых, мы опять же определяем класс, который наследуется от `TestCase`'а и определяем новую функцию `setUp`, с который вы еще не знакомы. Функция `setUp` призвана, как и соответствует ее название, \"засетапить\" окружение, которое будет работать во время исполнения тестовой функции. Таким образом, если нам нужно работать, например, с объектом `Asteroid`'а, мы можем в начале исполнения каждой функции создавать этот объект `Asteroid`'а, чтобы не дублировать этот код каждый раз в начале наших тестовых функций, или мы можем создавать другие какие-то объекты или как-то наши данные готовить. Существует симметричный метод, который называется `tearDown`, который позволяет закрывать какие-то ресурсы, удалять объекты в конце каждой тестовой функции.\n",
    "\n",
    "Итак, давайте напишем наши две тестовые функции, которые будут проверять `test_name`, то есть `diameter`. Однако, что если мы тестируем наши функции в окружении без Интернета, как я вам уже говорил. На помощь нам может прийти механизм `mock`'ов и модуль `unittest.mock`, который позволяет подменять какую-то функциональность, подменять какие-то функции другими. Таким образом, мы можем на самом деле не ходить в Интернет, а можем, например, читать информацию из файла. Я заранее скачал данные об астероиде `Apophis` в специальную фикстуру, текстовый файл. Это просто точно то же самое, что и возвращает наш `api`, только оно лежит в файле. Мы подменим функцию, которая идет в Интернет `get_data` функцией, которая просто читает из файла. Таким образом мы не будет ходить в Интернет во время тестов. Делается это с помощью декоратора `patch` довольно просто. Мы можем проверять внутри нашей тестовой функции определенные условия. В данном случае мы проверяем действительно ли имя астероида, которое мы распарсили, в данном случае из файла, действительно оно корректно, действительно ли оно `Apophis` и проверять размер нашего астероида - действительно ли он равен почти 700 метрам.\n",
    "\n",
    "Итак, давайте запустим наш `TestCase`. Сделаем это точно также:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "dfc3003d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "..\r\n",
      "----------------------------------------------------------------------\r\n",
      "Ran 2 tests in 0.003s\r\n",
      "\r\n",
      "OK\r\n"
     ]
    }
   ],
   "source": [
    "! python -m unittest test_asteroid.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "797af4b7",
   "metadata": {},
   "source": [
    "Да, наши тесты прошли, все замечательно, наш класс работает корректно. Скорее всего вручную тесты вы не будете запускать. Это будет запускать какая-то автоматическая система.\n",
    "\n",
    "Существует также у `unittest`'а возможность автоматического нахождения тестов, которые лежат, например, в директории `tests`. Очень редко приходится вручную запускать конкретные файлы с тестами.\n",
    "\n",
    "Итак, что же мы сделали? Мы написали тест для нашего класса. Мы действительно теперь знаем, что у нас наши атрибуты `name` и `diameter` работают корректно. Что ж, мы научились тестировать наш код, и пожалуйста пишите тесты и не бойтесь этого. Удачи."
   ]
  }
 ],
 "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.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
