{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Test Fixtures: setUp and tearDown\n",
    "\n",
    "**Time commitment**: 20–25 minutes\n",
    "\n",
    "**What you'll learn:**\n",
    "- What test fixtures are and why they're useful\n",
    "- How to use `setUp()` and `tearDown()` for instance-level fixtures\n",
    "- How to use `setUpClass()` and `tearDownClass()` for class-level fixtures\n",
    "- When to use module-level fixtures\n",
    "- Best practices for writing clean, maintainable tests\n",
    "\n",
    "**Prerequisites:**\n",
    "- Completed [Your First Test](https://agilearn.co.uk/guides/unit-testing/learn/01-your-first-test)\n",
    "- Completed [Testing Thoroughly](https://agilearn.co.uk/guides/unit-testing/learn/02-testing-thoroughly)\n",
    "- Basic understanding of classes and objects in Python"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## What are test fixtures?\n",
    "\n",
    "A **test fixture** is the preparation needed to run tests. This includes:\n",
    "\n",
    "- Creating objects or data structures\n",
    "- Opening files or database connections\n",
    "- Setting up temporary directories\n",
    "- Configuring mock objects\n",
    "\n",
    "Test fixtures ensure each test starts in a known, clean state.\n",
    "\n",
    "### The problem: Repetitive setup code\n",
    "\n",
    "Without fixtures, you might write code like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import unittest\n",
    "\n",
    "class ShoppingCart:\n",
    "    \"\"\"A simple shopping cart to demonstrate fixtures.\"\"\"\n",
    "    \n",
    "    def __init__(self):\n",
    "        self.items = []\n",
    "    \n",
    "    def add_item(self, item, price):\n",
    "        \"\"\"Add an item to the cart.\"\"\"\n",
    "        self.items.append({\"item\": item, \"price\": price})\n",
    "    \n",
    "    def total(self):\n",
    "        \"\"\"Calculate the total cost of all items.\"\"\"\n",
    "        return sum(item[\"price\"] for item in self.items)\n",
    "    \n",
    "    def clear(self):\n",
    "        \"\"\"Remove all items from the cart.\"\"\"\n",
    "        self.items = []\n",
    "\n",
    "\n",
    "class TestShoppingCartWithoutFixtures(unittest.TestCase):\n",
    "    \"\"\"Tests with repetitive setup - not using fixtures.\"\"\"\n",
    "    \n",
    "    def test_add_single_item(self):\n",
    "        \"\"\"Test adding a single item to the cart.\"\"\"\n",
    "        cart = ShoppingCart()  # Repetitive!\n",
    "        cart.add_item(\"Apple\", 0.99)\n",
    "        self.assertEqual(len(cart.items), 1)\n",
    "    \n",
    "    def test_calculate_total(self):\n",
    "        \"\"\"Test calculating the total cost.\"\"\"\n",
    "        cart = ShoppingCart()  # Repetitive!\n",
    "        cart.add_item(\"Apple\", 0.99)\n",
    "        cart.add_item(\"Banana\", 0.59)\n",
    "        self.assertEqual(cart.total(), 1.58)\n",
    "    \n",
    "    def test_clear_cart(self):\n",
    "        \"\"\"Test clearing the cart.\"\"\"\n",
    "        cart = ShoppingCart()  # Repetitive!\n",
    "        cart.add_item(\"Apple\", 0.99)\n",
    "        cart.clear()\n",
    "        self.assertEqual(len(cart.items), 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice how we create a new `ShoppingCart()` in every test method? This is repetitive and violates the DRY (Don't Repeat Yourself) principle.\n",
    "\n",
    "## Using setUp() to eliminate repetition\n",
    "\n",
    "The `setUp()` method runs **before each test method**. Use it to create objects or prepare data that every test needs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestShoppingCartWithSetUp(unittest.TestCase):\n",
    "    \"\"\"Tests using setUp() to prepare fixtures.\"\"\"\n",
    "    \n",
    "    def setUp(self):\n",
    "        \"\"\"Run before each test method - creates a fresh cart.\"\"\"\n",
    "        self.cart = ShoppingCart()\n",
    "        print(f\"setUp: Created new cart (id: {id(self.cart)})\")\n",
    "    \n",
    "    def test_add_single_item(self):\n",
    "        \"\"\"Test adding a single item to the cart.\"\"\"\n",
    "        self.cart.add_item(\"Apple\", 0.99)\n",
    "        self.assertEqual(len(self.cart.items), 1)\n",
    "        print(f\"test_add_single_item: Cart has {len(self.cart.items)} item(s)\")\n",
    "    \n",
    "    def test_calculate_total(self):\n",
    "        \"\"\"Test calculating the total cost.\"\"\"\n",
    "        self.cart.add_item(\"Apple\", 0.99)\n",
    "        self.cart.add_item(\"Banana\", 0.59)\n",
    "        self.assertEqual(self.cart.total(), 1.58)\n",
    "        print(f\"test_calculate_total: Total is £{self.cart.total():.2f}\")\n",
    "    \n",
    "    def test_clear_cart(self):\n",
    "        \"\"\"Test clearing the cart.\"\"\"\n",
    "        self.cart.add_item(\"Apple\", 0.99)\n",
    "        self.cart.clear()\n",
    "        self.assertEqual(len(self.cart.items), 0)\n",
    "        print(f\"test_clear_cart: Cart cleared, now has {len(self.cart.items)} item(s)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's run these tests to see setUp() in action:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Run the tests\n",
    "suite = unittest.TestLoader().loadTestsFromTestCase(TestShoppingCartWithSetUp)\n",
    "runner = unittest.TextTestRunner(verbosity=2)\n",
    "runner.run(suite)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Key observations:**\n",
    "\n",
    "1. `setUp()` runs **before each test method**\n",
    "2. Each test gets a **fresh cart** (notice different object IDs)\n",
    "3. Tests are **isolated** - changes in one test don't affect others\n",
    "4. The test code is **cleaner** - no repetitive cart creation\n",
    "\n",
    "## Using tearDown() to clean up\n",
    "\n",
    "The `tearDown()` method runs **after each test method**, regardless of whether the test passed or failed. Use it to clean up resources like:\n",
    "\n",
    "- Closing files\n",
    "- Closing database connections\n",
    "- Deleting temporary files\n",
    "- Resetting global state\n",
    "\n",
    "Here's a practical example with file operations:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import tempfile\n",
    "import os\n",
    "from pathlib import Path\n",
    "\n",
    "class FileProcessor:\n",
    "    \"\"\"A simple file processor to demonstrate tearDown.\"\"\"\n",
    "    \n",
    "    def __init__(self, filepath):\n",
    "        self.filepath = filepath\n",
    "    \n",
    "    def write_lines(self, lines):\n",
    "        \"\"\"Write lines to the file.\"\"\"\n",
    "        with open(self.filepath, 'w') as f:\n",
    "            f.write('\\n'.join(lines))\n",
    "    \n",
    "    def read_lines(self):\n",
    "        \"\"\"Read lines from the file.\"\"\"\n",
    "        with open(self.filepath, 'r') as f:\n",
    "            return [line.strip() for line in f.readlines()]\n",
    "    \n",
    "    def count_lines(self):\n",
    "        \"\"\"Count the number of lines in the file.\"\"\"\n",
    "        return len(self.read_lines())\n",
    "\n",
    "\n",
    "class TestFileProcessor(unittest.TestCase):\n",
    "    \"\"\"Tests for FileProcessor with proper cleanup.\"\"\"\n",
    "    \n",
    "    def setUp(self):\n",
    "        \"\"\"Create a temporary file before each test.\"\"\"\n",
    "        # Create a temporary file\n",
    "        self.temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt')\n",
    "        self.temp_filepath = self.temp_file.name\n",
    "        self.temp_file.close()\n",
    "        \n",
    "        # Create the processor\n",
    "        self.processor = FileProcessor(self.temp_filepath)\n",
    "        print(f\"setUp: Created temp file at {self.temp_filepath}\")\n",
    "    \n",
    "    def tearDown(self):\n",
    "        \"\"\"Delete the temporary file after each test.\"\"\"\n",
    "        if os.path.exists(self.temp_filepath):\n",
    "            os.remove(self.temp_filepath)\n",
    "            print(f\"tearDown: Deleted temp file {self.temp_filepath}\")\n",
    "    \n",
    "    def test_write_and_read_lines(self):\n",
    "        \"\"\"Test writing and reading lines.\"\"\"\n",
    "        lines = [\"Hello\", \"World\", \"Testing\"]\n",
    "        self.processor.write_lines(lines)\n",
    "        result = self.processor.read_lines()\n",
    "        self.assertEqual(result, lines)\n",
    "    \n",
    "    def test_count_lines(self):\n",
    "        \"\"\"Test counting lines in a file.\"\"\"\n",
    "        lines = [\"Line 1\", \"Line 2\", \"Line 3\", \"Line 4\"]\n",
    "        self.processor.write_lines(lines)\n",
    "        self.assertEqual(self.processor.count_lines(), 4)\n",
    "    \n",
    "    def test_empty_file(self):\n",
    "        \"\"\"Test reading an empty file.\"\"\"\n",
    "        self.processor.write_lines([])\n",
    "        result = self.processor.read_lines()\n",
    "        self.assertEqual(result, [''])  # Empty file has one empty line"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's run these tests:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Run the tests\n",
    "suite = unittest.TestLoader().loadTestsFromTestCase(TestFileProcessor)\n",
    "runner = unittest.TextTestRunner(verbosity=2)\n",
    "runner.run(suite)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Key observations:**\n",
    "\n",
    "1. `setUp()` creates a temporary file before each test\n",
    "2. `tearDown()` **always** runs, even if a test fails\n",
    "3. Each test gets its own fresh file\n",
    "4. No leftover files after tests complete\n",
    "\n",
    "## Class-level fixtures: setUpClass() and tearDownClass()\n",
    "\n",
    "Sometimes setup is expensive (for example, database connections, loading large datasets). For these cases, use **class-level fixtures** that run once per test class.\n",
    "\n",
    "- `setUpClass()` runs **once** before any tests in the class\n",
    "- `tearDownClass()` runs **once** after all tests in the class\n",
    "\n",
    "These methods must be decorated with `@classmethod`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Database:\n",
    "    \"\"\"Mock database to demonstrate class-level fixtures.\"\"\"\n",
    "    \n",
    "    def __init__(self, db_name):\n",
    "        self.db_name = db_name\n",
    "        self.data = {}\n",
    "        print(f\"Database '{db_name}' connected\")\n",
    "    \n",
    "    def insert(self, key, value):\n",
    "        \"\"\"Insert a key-value pair.\"\"\"\n",
    "        self.data[key] = value\n",
    "    \n",
    "    def get(self, key):\n",
    "        \"\"\"Get a value by key.\"\"\"\n",
    "        return self.data.get(key)\n",
    "    \n",
    "    def close(self):\n",
    "        \"\"\"Close the database connection.\"\"\"\n",
    "        print(f\"Database '{self.db_name}' closed\")\n",
    "        self.data = None\n",
    "\n",
    "\n",
    "class TestDatabaseWithClassFixtures(unittest.TestCase):\n",
    "    \"\"\"Tests using class-level fixtures for expensive setup.\"\"\"\n",
    "    \n",
    "    @classmethod\n",
    "    def setUpClass(cls):\n",
    "        \"\"\"Run once before all tests - create database connection.\"\"\"\n",
    "        print(\"\\nsetUpClass: Creating database connection...\")\n",
    "        cls.db = Database(\"test_db\")\n",
    "        # Load initial test data (imagine this is expensive)\n",
    "        cls.db.insert(\"user:1\", {\"name\": \"Alice\", \"role\": \"admin\"})\n",
    "        cls.db.insert(\"user:2\", {\"name\": \"Bob\", \"role\": \"user\"})\n",
    "        print(\"setUpClass: Database ready with test data\\n\")\n",
    "    \n",
    "    @classmethod\n",
    "    def tearDownClass(cls):\n",
    "        \"\"\"Run once after all tests - close database connection.\"\"\"\n",
    "        print(\"\\ntearDownClass: Closing database connection...\")\n",
    "        cls.db.close()\n",
    "        print(\"tearDownClass: Cleanup complete\\n\")\n",
    "    \n",
    "    def setUp(self):\n",
    "        \"\"\"Run before each test - create a snapshot of current data.\"\"\"\n",
    "        # Store initial state so we can verify tests don't affect each other\n",
    "        self.initial_data_count = len(self.db.data)\n",
    "        print(f\"setUp: Database has {self.initial_data_count} records\")\n",
    "    \n",
    "    def test_get_user_alice(self):\n",
    "        \"\"\"Test retrieving Alice's record.\"\"\"\n",
    "        user = self.db.get(\"user:1\")\n",
    "        self.assertEqual(user[\"name\"], \"Alice\")\n",
    "        self.assertEqual(user[\"role\"], \"admin\")\n",
    "        print(\"test_get_user_alice: Retrieved Alice successfully\")\n",
    "    \n",
    "    def test_get_user_bob(self):\n",
    "        \"\"\"Test retrieving Bob's record.\"\"\"\n",
    "        user = self.db.get(\"user:2\")\n",
    "        self.assertEqual(user[\"name\"], \"Bob\")\n",
    "        self.assertEqual(user[\"role\"], \"user\")\n",
    "        print(\"test_get_user_bob: Retrieved Bob successfully\")\n",
    "    \n",
    "    def test_insert_new_user(self):\n",
    "        \"\"\"Test inserting a new user.\"\"\"\n",
    "        self.db.insert(\"user:3\", {\"name\": \"Charlie\", \"role\": \"user\"})\n",
    "        user = self.db.get(\"user:3\")\n",
    "        self.assertEqual(user[\"name\"], \"Charlie\")\n",
    "        print(f\"test_insert_new_user: Database now has {len(self.db.data)} records\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's run these tests:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Run the tests\n",
    "suite = unittest.TestLoader().loadTestsFromTestCase(TestDatabaseWithClassFixtures)\n",
    "runner = unittest.TextTestRunner(verbosity=2)\n",
    "runner.run(suite)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Key observations:**\n",
    "\n",
    "1. `setUpClass()` runs **once** at the start\n",
    "2. `setUp()` still runs before **each test**\n",
    "3. All tests share the **same database instance**\n",
    "4. `tearDownClass()` runs **once** at the end\n",
    "5. The third test adds data that persists for the class\n",
    "\n",
    "**Warning:** Class-level fixtures can lead to tests affecting each other if you're not careful. Use them only when:\n",
    "- Setup is genuinely expensive (for example, database connections, loading large files)\n",
    "- Tests only **read** from the shared resource\n",
    "- Or you carefully reset state in `setUp()`\n",
    "\n",
    "## Module-level fixtures\n",
    "\n",
    "For even more expensive setup, use module-level fixtures:\n",
    "\n",
    "```python\n",
    "def setUpModule():\n",
    "    \"\"\"Run once when the test module is loaded.\"\"\"\n",
    "    # For example: start a test server, load a huge dataset\n",
    "    pass\n",
    "\n",
    "def tearDownModule():\n",
    "    \"\"\"Run once when the test module finishes.\"\"\"\n",
    "    # For example: stop the test server, clean up resources\n",
    "    pass\n",
    "```\n",
    "\n",
    "These run once per **module** (test file), shared across all test classes in that file.\n",
    "\n",
    "## Best practices for test fixtures\n",
    "\n",
    "### 1. Keep setUp() simple and fast\n",
    "\n",
    "```python\n",
    "# Good - simple and fast\n",
    "def setUp(self):\n",
    "    self.cart = ShoppingCart()\n",
    "    self.user = {\"name\": \"Alice\", \"id\": 1}\n",
    "\n",
    "# Avoid - slow operations in setUp()\n",
    "def setUp(self):\n",
    "    self.api_client = connect_to_production_api()  # Slow!\n",
    "    self.large_dataset = load_1gb_file()           # Slow!\n",
    "```\n",
    "\n",
    "### 2. Each test should be independent\n",
    "\n",
    "```python\n",
    "# Good - each test is independent\n",
    "def setUp(self):\n",
    "    self.cart = ShoppingCart()\n",
    "\n",
    "def test_add_item(self):\n",
    "    self.cart.add_item(\"Apple\", 0.99)\n",
    "    self.assertEqual(len(self.cart.items), 1)\n",
    "\n",
    "def test_total(self):\n",
    "    # Starts with a fresh cart - doesn't depend on test_add_item\n",
    "    self.cart.add_item(\"Banana\", 0.59)\n",
    "    self.assertEqual(self.cart.total(), 0.59)\n",
    "\n",
    "# Avoid - tests depending on each other\n",
    "def test_add_item(self):\n",
    "    self.cart.add_item(\"Apple\", 0.99)\n",
    "\n",
    "def test_total(self):\n",
    "    # BAD: Assumes test_add_item ran first!\n",
    "    self.assertEqual(self.cart.total(), 0.99)\n",
    "```\n",
    "\n",
    "### 3. Always clean up in tearDown()\n",
    "\n",
    "```python\n",
    "# Good - reliable cleanup\n",
    "def tearDown(self):\n",
    "    if os.path.exists(self.temp_file):\n",
    "        os.remove(self.temp_file)\n",
    "    \n",
    "    if self.db_connection and self.db_connection.is_connected():\n",
    "        self.db_connection.close()\n",
    "\n",
    "# Avoid - forgetting to clean up\n",
    "def tearDown(self):\n",
    "    pass  # Oops, temp files will accumulate!\n",
    "```\n",
    "\n",
    "### 4. Use class fixtures sparingly\n",
    "\n",
    "```python\n",
    "# Good use of class fixtures - expensive, read-only setup\n",
    "@classmethod\n",
    "def setUpClass(cls):\n",
    "    cls.large_dataset = load_100mb_reference_data()  # Expensive, read-only\n",
    "\n",
    "# Questionable - mutable shared state\n",
    "@classmethod\n",
    "def setUpClass(cls):\n",
    "    cls.cart = ShoppingCart()  # Tests will modify this!\n",
    "```\n",
    "\n",
    "### 5. Name your fixtures clearly\n",
    "\n",
    "```python\n",
    "# Good - clear fixture names\n",
    "def setUp(self):\n",
    "    self.empty_cart = ShoppingCart()\n",
    "    self.admin_user = User(\"admin\", role=\"admin\")\n",
    "    self.test_data_file = \"/tmp/test_data.json\"\n",
    "\n",
    "# Avoid - vague names\n",
    "def setUp(self):\n",
    "    self.obj = ShoppingCart()\n",
    "    self.thing = User(\"admin\")\n",
    "    self.file = \"/tmp/test_data.json\"\n",
    "```\n",
    "\n",
    "## Hands-on exercise: Bank account with fixtures\n",
    "\n",
    "Let's create a bank account class and write tests using fixtures.\n",
    "\n",
    "**Your task:**\n",
    "1. Complete the test class below\n",
    "2. Add `setUp()` to create a fresh account before each test\n",
    "3. Write tests for deposit, withdraw, and balance\n",
    "4. Verify tests are independent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    \"\"\"A simple bank account for the exercise.\"\"\"\n",
    "    \n",
    "    def __init__(self, initial_balance=0):\n",
    "        self.balance = initial_balance\n",
    "        self.transaction_count = 0\n",
    "    \n",
    "    def deposit(self, amount):\n",
    "        \"\"\"Deposit money into the account.\"\"\"\n",
    "        if amount <= 0:\n",
    "            raise ValueError(\"Deposit amount must be positive\")\n",
    "        self.balance += amount\n",
    "        self.transaction_count += 1\n",
    "    \n",
    "    def withdraw(self, amount):\n",
    "        \"\"\"Withdraw money from the account.\"\"\"\n",
    "        if amount <= 0:\n",
    "            raise ValueError(\"Withdrawal amount must be positive\")\n",
    "        if amount > self.balance:\n",
    "            raise ValueError(\"Insufficient funds\")\n",
    "        self.balance -= amount\n",
    "        self.transaction_count += 1\n",
    "    \n",
    "    def get_balance(self):\n",
    "        \"\"\"Get the current balance.\"\"\"\n",
    "        return self.balance\n",
    "\n",
    "\n",
    "# TODO: Complete this test class\n",
    "class TestBankAccount(unittest.TestCase):\n",
    "    \"\"\"Tests for BankAccount using fixtures.\"\"\"\n",
    "    \n",
    "    def setUp(self):\n",
    "        \"\"\"Create a fresh bank account before each test.\"\"\"\n",
    "        # TODO: Create a BankAccount with £100 initial balance\n",
    "        pass\n",
    "    \n",
    "    def test_initial_balance(self):\n",
    "        \"\"\"Test that the account starts with the correct balance.\"\"\"\n",
    "        # TODO: Assert that balance is £100\n",
    "        pass\n",
    "    \n",
    "    def test_deposit(self):\n",
    "        \"\"\"Test depositing money.\"\"\"\n",
    "        # TODO: Deposit £50 and verify balance is £150\n",
    "        pass\n",
    "    \n",
    "    def test_withdraw(self):\n",
    "        \"\"\"Test withdrawing money.\"\"\"\n",
    "        # TODO: Withdraw £30 and verify balance is £70\n",
    "        pass\n",
    "    \n",
    "    def test_withdraw_insufficient_funds(self):\n",
    "        \"\"\"Test that withdrawing too much raises an error.\"\"\"\n",
    "        # TODO: Attempt to withdraw £200 and verify ValueError is raised\n",
    "        pass\n",
    "    \n",
    "    def test_multiple_transactions(self):\n",
    "        \"\"\"Test multiple deposits and withdrawals.\"\"\"\n",
    "        # TODO: Deposit £50, withdraw £30, deposit £20\n",
    "        # Verify final balance is £140\n",
    "        # Verify transaction_count is 3\n",
    "        pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Solution\n",
    "\n",
    "Here's the completed test class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TestBankAccountSolution(unittest.TestCase):\n",
    "    \"\"\"Tests for BankAccount using fixtures - SOLUTION.\"\"\"\n",
    "    \n",
    "    def setUp(self):\n",
    "        \"\"\"Create a fresh bank account before each test.\"\"\"\n",
    "        self.account = BankAccount(initial_balance=100)\n",
    "    \n",
    "    def test_initial_balance(self):\n",
    "        \"\"\"Test that the account starts with the correct balance.\"\"\"\n",
    "        self.assertEqual(self.account.get_balance(), 100)\n",
    "    \n",
    "    def test_deposit(self):\n",
    "        \"\"\"Test depositing money.\"\"\"\n",
    "        self.account.deposit(50)\n",
    "        self.assertEqual(self.account.get_balance(), 150)\n",
    "    \n",
    "    def test_withdraw(self):\n",
    "        \"\"\"Test withdrawing money.\"\"\"\n",
    "        self.account.withdraw(30)\n",
    "        self.assertEqual(self.account.get_balance(), 70)\n",
    "    \n",
    "    def test_withdraw_insufficient_funds(self):\n",
    "        \"\"\"Test that withdrawing too much raises an error.\"\"\"\n",
    "        with self.assertRaises(ValueError) as cm:\n",
    "            self.account.withdraw(200)\n",
    "        self.assertEqual(str(cm.exception), \"Insufficient funds\")\n",
    "    \n",
    "    def test_multiple_transactions(self):\n",
    "        \"\"\"Test multiple deposits and withdrawals.\"\"\"\n",
    "        self.account.deposit(50)   # 100 + 50 = 150\n",
    "        self.account.withdraw(30)  # 150 - 30 = 120\n",
    "        self.account.deposit(20)   # 120 + 20 = 140\n",
    "        \n",
    "        self.assertEqual(self.account.get_balance(), 140)\n",
    "        self.assertEqual(self.account.transaction_count, 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's run the solution tests:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Run the tests\n",
    "suite = unittest.TestLoader().loadTestsFromTestCase(TestBankAccountSolution)\n",
    "runner = unittest.TextTestRunner(verbosity=2)\n",
    "runner.run(suite)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "You've learnt how to use test fixtures to write cleaner, more maintainable tests:\n",
    "\n",
    "### Key concepts\n",
    "\n",
    "| Fixture Type | Runs | Use For |\n",
    "|-------------|------|----------|\n",
    "| `setUp()` | Before each test method | Creating fresh objects, opening files |\n",
    "| `tearDown()` | After each test method | Closing files, cleaning up resources |\n",
    "| `setUpClass()` | Once before all tests in class | Expensive setup (database connections) |\n",
    "| `tearDownClass()` | Once after all tests in class | Closing shared resources |\n",
    "| `setUpModule()` | Once when module loads | Very expensive setup (test servers) |\n",
    "| `tearDownModule()` | Once when module finishes | Cleaning up module-level resources |\n",
    "\n",
    "### Best practices\n",
    "\n",
    "1. **Use `setUp()` to eliminate repetitive code** - create fresh objects for each test\n",
    "2. **Always clean up in `tearDown()`** - close files, connections, delete temp files\n",
    "3. **Keep tests independent** - don't rely on test execution order\n",
    "4. **Use class fixtures sparingly** - only for genuinely expensive, read-only setup\n",
    "5. **Name fixtures clearly** - `self.empty_cart` is better than `self.cart`\n",
    "\n",
    "### Common pitfalls\n",
    "\n",
    "- **Forgetting `@classmethod`** on `setUpClass()` and `tearDownClass()`\n",
    "- **Slow operations in `setUp()`** - runs before every test, making tests slow\n",
    "- **Mutable shared state in class fixtures** - tests can affect each other\n",
    "- **Not cleaning up in `tearDown()`** - temporary files and connections accumulate\n",
    "\n",
    "## Next steps\n",
    "\n",
    "- **[Run Tests in Jupyter](https://agilearn.co.uk/guides/unit-testing/recipes/run-tests-in-jupyter)**: Learn how to run your tests effectively\n",
    "- **[Test Exceptions](https://agilearn.co.uk/guides/unit-testing/recipes/test-exceptions)**: Learn how to test error conditions\n",
    "- **[Avoid Common Mistakes](https://agilearn.co.uk/guides/unit-testing/recipes/avoid-common-testing-mistakes)**: Learn common testing pitfalls\n",
    "- **[Assertions Reference](https://agilearn.co.uk/guides/unit-testing/reference/assertions)**: Complete guide to all unittest assertions\n",
    "\n",
    "## Further practice\n",
    "\n",
    "Try these exercises to reinforce your learning:\n",
    "\n",
    "1. **File manager tests**: Create a class that manages files in a directory, using `setUp()` to create a temp directory and `tearDown()` to delete it\n",
    "\n",
    "2. **User session tests**: Create a user session class with login/logout, using fixtures to ensure each test starts logged out\n",
    "\n",
    "3. **Configuration tests**: Create a configuration loader that reads from a file, using class fixtures to load configuration once\n",
    "\n",
    "4. **API client tests**: Create a mock API client, using `setUpClass()` to \"connect\" once and `setUp()` to reset the client state before each test"
   ]
  }
 ],
 "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
}