{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Python - PyTest\n",
    "\n",
    "- https://docs.pytest.org/en/latest/\n",
    "- https://www.youtube.com/watch?time_continue=383&v=pX1_I_sEi8k\n",
    "- [Effective Python Testing With Pytest](https://realpython.com/pytest-python-testing/)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "## Arrange-Act-Assert model:\n",
    "\n",
    "- **Arrange**, or set up, the conditions for the test\n",
    "- **Act** by calling some function or method\n",
    "- **Assert** that some end condition is true"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "%config Completer.use_jedi = False\n",
    "\n",
    "import pytest\n",
    "import ipytest\n",
    "ipytest.autoconfig()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "...                                                                  [100%]\n",
      "3 passed in 0.05s\n"
     ]
    }
   ],
   "source": [
    "%%run_pytest[clean]\n",
    "\n",
    "def test_uppercase():\n",
    "    assert \"loud noises\".upper() == \"LOUD NOISES\"\n",
    "\n",
    "def test_reversed():\n",
    "    assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1]\n",
    "\n",
    "def test_some_primes():\n",
    "    assert 37 in {\n",
    "        num\n",
    "        for num in range(1, 50)\n",
    "        if num != 1 and not any([num % div == 0 for div in range(2, num)])\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## State and Dependency Management -- `fixtures`\n",
    "\n",
    "- `pytest` fixtures are functions that create data or test doubles or initialize some system state for the test suite.\n",
    "- Any test that wants to use a fixture must explicitly accept it as an argument, so dependencies are always stated up front."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "..\n",
      "2 passed in 0.01s\n"
     ]
    }
   ],
   "source": [
    "%%run_pytest[clean] -s\n",
    "\n",
    "def format_data_for_display(people):\n",
    "    # Implement this!\n",
    "    out = []\n",
    "    for person in people:\n",
    "        display = f'{person[\"given_name\"]} {person[\"family_name\"]}: {person[\"title\"]}'\n",
    "        out.append(display)\n",
    "        \n",
    "    return out\n",
    "\n",
    "\n",
    "def format_data_for_excel(people):\n",
    "    header = \"given,family,title\"\n",
    "    out = [header]\n",
    "    for person in people:\n",
    "        display = f'{person[\"given_name\"]},{person[\"family_name\"]},{person[\"title\"]}'\n",
    "        out.append(display)\n",
    "\n",
    "    out = \"\\n\".join(out) + \"\\n\"\n",
    "    return out\n",
    "\n",
    "\n",
    "# def test_format_data_for_display():\n",
    "#     people = [\n",
    "#         {\n",
    "#             \"given_name\": \"Alfonsa\",\n",
    "#             \"family_name\": \"Ruiz\",\n",
    "#             \"title\": \"Senior Software Engineer\",\n",
    "#         },\n",
    "#         {\n",
    "#             \"given_name\": \"Sayid\",\n",
    "#             \"family_name\": \"Khan\",\n",
    "#             \"title\": \"Project Manager\",\n",
    "#         },\n",
    "#     ]\n",
    "\n",
    "#     assert format_data_for_display(people) == [\n",
    "#         \"Alfonsa Ruiz: Senior Software Engineer\",\n",
    "#         \"Sayid Khan: Project Manager\",\n",
    "#     ]\n",
    "\n",
    "\n",
    "@pytest.fixture\n",
    "def example_people_data():\n",
    "    return [\n",
    "        {\n",
    "            \"given_name\": \"Alfonsa\",\n",
    "            \"family_name\": \"Ruiz\",\n",
    "            \"title\": \"Senior Software Engineer\",\n",
    "        },\n",
    "        {\n",
    "            \"given_name\": \"Sayid\",\n",
    "            \"family_name\": \"Khan\",\n",
    "            \"title\": \"Project Manager\",\n",
    "        },\n",
    "    ]\n",
    "\n",
    "\n",
    "def test_format_data_for_display(example_people_data):\n",
    "    assert format_data_for_display(example_people_data) == [\n",
    "        \"Alfonsa Ruiz: Senior Software Engineer\",\n",
    "        \"Sayid Khan: Project Manager\",\n",
    "    ]\n",
    "\n",
    "def test_format_data_for_excel(example_people_data):\n",
    "    assert format_data_for_excel(example_people_data) == \"\"\"given,family,title\n",
    "Alfonsa,Ruiz,Senior Software Engineer\n",
    "Sayid,Khan,Project Manager\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### When to Avoid Fixtures?\n",
    "\n",
    "Fixtures are great for extracting data or objects that you use across multiple tests. They aren’t always as good for tests that require slight variations in the data. Littering your test suite with fixtures is no better than littering it with plain data or objects. It might even be worse because of the added layer of indirection.\n",
    "\n",
    "As with most abstractions, it takes some practice and thought to find the right level of fixture use.\n",
    "\n",
    "\n",
    "#### Fixtures at Scale\n",
    "\n",
    "You can move fixtures from test modules into more general fixture-related modules. That way, you can import them back into any test modules that need them. This is a good approach when you find yourself using a fixture repeatedly throughout your project.\n",
    "\n",
    "`pytest` looks for `conftest.py` modules throughout the directory structure. Each `conftest.py` provides configuration for the file tree `pytest` finds it in. You can use any fixtures that are defined in a particular `conftest.py` throughout the file’s parent directory and in any subdirectories. This is a great place to put your most widely used fixtures."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### monkeypatch\n",
    "https://docs.pytest.org/en/latest/monkeypatch.html\n",
    "\n",
    "The monkeypatch fixture helps you to safely set/delete an attribute, dictionary item or environment variable, or to modify `sys.path` for importing.\n",
    "\n",
    "```py\n",
    "monkeypatch.setattr(obj, name, value, raising=True)\n",
    "monkeypatch.delattr(obj, name, raising=True)\n",
    "monkeypatch.setitem(mapping, name, value)\n",
    "monkeypatch.delitem(obj, name, raising=True)\n",
    "monkeypatch.setenv(name, value, prepend=False)\n",
    "monkeypatch.delenv(name, raising=True)\n",
    "monkeypatch.syspath_prepend(path)\n",
    "monkeypatch.chdir(path)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%script false --no-raise-error\n",
    "# conftest.py\n",
    "\n",
    "import pytest\n",
    "import requests\n",
    "\n",
    "@pytest.fixture(autouse=True)\n",
    "def disable_network_calls(monkeypatch):\n",
    "    \"\"\"\n",
    "    By placing disable_network_calls() in conftest.py and adding the autouse=True option,\n",
    "    you ensure that network calls will be disabled in every test across the suite.\n",
    "    Any test that executes code calling requests.get() will raise a RuntimeError indicating\n",
    "    that an unexpected network call would have occurred.\n",
    "    \"\"\"\n",
    "    def stunted_get():\n",
    "        raise RuntimeError(\"Network access not allowed during testing!\")\n",
    "    monkeypatch.setattr(requests, \"get\", lambda *args, **kwargs: stunted_get())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Test Filtering\n",
    "\n",
    "As your test suite grows, you may find that you want to run just a few tests on a feature and save the full suite for later. pytest provides a few ways of doing this:\n",
    "\n",
    "- **Name-based filtering**: You can limit pytest to running only those tests whose fully qualified names match a particular expression. You can do this with the `-k` parameter.\n",
    "- **Directory scoping**: By default, pytest will run only those tests that are in or under the current directory.\n",
    "- **Test categorization**: pytest can include or exclude tests from particular categories that you define. You can do this with the `-m` parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "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": 2
}
