{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# How do I run `unittest` tests inside a Jupyter notebook?\n",
    "\n",
    "Notebooks don't have the command-line test runner that `python -m unittest` gives you, so the usual instructions for running a `TestCase` don't quite work as written. The trick is to call `unittest.main()` with two specific arguments — `argv` to stop it choking on the notebook's argv, and `exit=False` to stop it killing the kernel.\n",
    "\n",
    "This recipe gives you the exact one-line invocation to drop under your test class, plus the variants for verbose output and running just one test."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import unittest\n",
    "\n",
    "\n",
    "class TestArithmetic(unittest.TestCase):\n",
    "    def test_addition(self):\n",
    "        self.assertEqual(2 + 2, 4)\n",
    "\n",
    "    def test_subtraction(self):\n",
    "        self.assertEqual(5 - 3, 2)\n",
    "\n",
    "\n",
    "# This is the line that makes unittest work in a notebook.\n",
    "# - argv=[''] stops unittest from trying to parse the kernel's argv\n",
    "# - exit=False stops it from calling sys.exit() and killing the kernel\n",
    "unittest.main(argv=[''], exit=False, verbosity=2)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Two variations: running a single test by name, and the difference between verbosity levels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import unittest\n",
    "\n",
    "\n",
    "class TestNames(unittest.TestCase):\n",
    "    def test_alpha(self):\n",
    "        self.assertTrue(\"a\".isalpha())\n",
    "\n",
    "    def test_digit(self):\n",
    "        self.assertTrue(\"1\".isdigit())\n",
    "\n",
    "    def test_space(self):\n",
    "        self.assertTrue(\" \".isspace())\n",
    "\n",
    "\n",
    "# Run just one test by passing its dotted name in argv.\n",
    "# The format is __main__.<TestClass>.<test_method>\n",
    "unittest.main(\n",
    "    argv=['', '__main__.TestNames.test_digit'],\n",
    "    exit=False,\n",
    "    verbosity=2,\n",
    ")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import unittest\n",
    "\n",
    "\n",
    "class TestVerbosity(unittest.TestCase):\n",
    "    def test_one(self):\n",
    "        self.assertEqual(1, 1)\n",
    "\n",
    "    def test_two(self):\n",
    "        self.assertEqual(2, 2)\n",
    "\n",
    "\n",
    "print(\"=== verbosity=0 (quiet) ===\")\n",
    "unittest.main(argv=[''], exit=False, verbosity=0)\n",
    "\n",
    "print(\"\\n=== verbosity=1 (default — one dot per test) ===\")\n",
    "unittest.main(argv=[''], exit=False, verbosity=1)\n",
    "\n",
    "print(\"\\n=== verbosity=2 (one line per test, with method name) ===\")\n",
    "unittest.main(argv=[''], exit=False, verbosity=2)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why it works\n",
    "\n",
    "`unittest.main()` is built for command-line use. By default it does two things that are useful from the shell and disastrous in a notebook.\n",
    "\n",
    "First, it reads `sys.argv` to figure out which tests to run. In a script that gives you `python my_tests.py TestSomething.test_one`. In a Jupyter kernel, `sys.argv` is a Jupyter-specific list — usually starts with `'/path/to/ipykernel_launcher.py'` and is followed by connection-file arguments — and `unittest.main` will try to interpret those as test names, then complain when they aren't. Passing `argv=['']` (a list containing one empty string, mimicking \"no command-line args\") sidesteps the whole mess.\n",
    "\n",
    "Second, it calls `sys.exit()` when finished, with the exit code reflecting whether tests passed. From the shell that's exactly right — your CI script needs the exit code. In a notebook, `sys.exit()` raises `SystemExit`, which terminates the kernel cell and (depending on the front-end) can leave the kernel in an awkward state. `exit=False` makes `unittest.main` return the test result instead of exiting.\n",
    "\n",
    "`verbosity=2` is worth the extra characters. The default `verbosity=1` prints one dot per test, which is fine for hundreds of tests but uninformative for the handful you typically run in a notebook. `verbosity=2` prints the test method name and PASS/FAIL/ERROR for each one, so you can see what actually ran without having to re-read the cell."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "`unittest.main()` discovers tests in the *current module*. In a notebook, \"the current module\" is `__main__`, which means it finds every `TestCase` subclass defined in any cell that's been run so far. That's usually what you want when you're iterating — re-define a class, re-run, see results — but it bites if you've left old test classes lying around in earlier cells. If you want to run only one class, use `unittest.TestLoader().loadTestsFromTestCase(MyTests)` and a `TextTestRunner` instead.\n",
    "\n",
    "`pytest` runs in notebooks too, via `ipytest` or just by writing tests in a `.py` file and shelling out. If your project already uses pytest for its real test suite, prefer that over re-learning `unittest` semantics for a notebook. Use this recipe when you want to demonstrate a `unittest`-based pattern without setting up an external runner.\n",
    "\n",
    "The notebook is *not* a substitute for a proper test suite. It's great for exploration — try a pattern, see what fails, iterate — but the tests that actually gate your code should live in a `tests/` folder and run from CI. Treat the notebook as a sketchbook."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related\n",
    "\n",
    "- [How to test exceptions and error handling](https://agilearn.co.uk/guides/unit-testing/recipes/test-exceptions) — `assertRaises` patterns, also runnable from a notebook.\n",
    "- [How to avoid common testing mistakes](https://agilearn.co.uk/guides/unit-testing/recipes/avoid-common-testing-mistakes) — the traps that catch people writing their first tests.\n",
    "- [`unittest` quick reference](https://agilearn.co.uk/guides/unit-testing/reference/unittest-quick-reference) — assertion methods, fixtures, and lifecycle hooks at a glance.\n",
    "- [Test naming conventions](https://agilearn.co.uk/guides/unit-testing/reference/test-naming-conventions) — why every test method needs the `test_` prefix."
   ]
  }
 ],
 "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.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}