Python unittest Skill Core Patterns Basic Test Assertions SubTest (Parameterized) Mocking Lifecycle Run: or Discover: Deep Patterns For advanced patterns, debugging guides, CI/CD integration, and best practices, see . ---

)\n\n def test_duplicate_email_raises(self):\n self.service.create(\"Alice\", \"[email protected]\")\n with self.assertRaises(ValueError) as ctx:\n self.service.create(\"Bob\", \"[email protected]\")\n self.assertIn(\"already exists\", str(ctx.exception))\n```\n\n## Advanced Mocking\n\n```python\nclass TestAPIClient(unittest.TestCase):\n @patch('myapp.client.requests.Session')\n def test_retries_on_failure(self, MockSession):\n session = MockSession.return_value\n session.get.side_effect = [\n ConnectionError(\"timeout\"),\n ConnectionError(\"timeout\"),\n MagicMock(status_code=200, json=lambda: {\"ok\": True})\n ]\n client = APIClient(retries=3)\n result = client.fetch(\"/data\")\n self.assertEqual(session.get.call_count, 3)\n self.assertTrue(result[\"ok\"])\n\n @patch.multiple('myapp.config', API_URL='http://test', API_KEY='fake-key')\n def test_uses_config(self):\n client = APIClient()\n self.assertEqual(client.base_url, 'http://test')\n\n def test_context_manager_mock(self):\n mock_file = MagicMock()\n mock_file.__enter__ = MagicMock(return_value=mock_file)\n mock_file.__exit__ = MagicMock(return_value=False)\n mock_file.read.return_value = '{\"key\": \"value\"}'\n with patch('builtins.open', return_value=mock_file):\n result = load_config('config.json')\n self.assertEqual(result['key'], 'value')\n\n @patch.object(UserService, 'validate', return_value=True)\n @patch.object(UserService, 'save')\n def test_chained_patches(self, mock_save, mock_validate):\n mock_save.return_value = User(id=1, name=\"Alice\")\n user = UserService().create_user(\"Alice\")\n mock_validate.assert_called_once()\n mock_save.assert_called_once()\n```\n\n## Subtests for Data-Driven Testing\n\n```python\nclass TestValidator(unittest.TestCase):\n def test_email_validation(self):\n cases = [\n (\"[email protected]\", True),\n (\"invalid\", False),\n (\"@missing.com\", False),\n (\"[email protected]\", False),\n (\"[email protected]\", True),\n ]\n for email, expected in cases:\n with self.subTest(email=email):\n self.assertEqual(validate_email(email), expected)\n```\n\n## Async Testing (Python 3.11+)\n\n```python\nclass TestAsyncService(unittest.IsolatedAsyncioTestCase):\n async def asyncSetUp(self):\n self.client = AsyncClient()\n await self.client.connect()\n\n async def asyncTearDown(self):\n await self.client.close()\n\n async def test_fetch_data(self):\n result = await self.client.fetch(\"/api/data\")\n self.assertEqual(result[\"status\"], \"ok\")\n\n async def test_concurrent_operations(self):\n import asyncio\n results = await asyncio.gather(\n self.client.fetch(\"/a\"), self.client.fetch(\"/b\")\n )\n self.assertTrue(all(r[\"status\"] == \"ok\" for r in results))\n```\n\n## Custom Test Runner & Discovery\n\n```python\n# Custom runner with XML output\nimport xmlrunner\n\nif __name__ == '__main__':\n unittest.main(\n testRunner=xmlrunner.XMLTestRunner(output='reports'),\n verbosity=2,\n failfast=True\n )\n\n# Custom discovery\nloader = unittest.TestLoader()\nsuite = unittest.TestSuite()\nsuite.addTests(loader.discover('tests', pattern='test_*.py'))\nsuite.addTests(loader.loadTestsFromName('tests.integration.test_api'))\nrunner = unittest.TextTestRunner(verbosity=2)\nrunner.run(suite)\n```\n\n## Anti-Patterns\n\n- ❌ `assertEqual(result, True)` → use `assertTrue(result)`\n- ❌ Bare `except` in tests — let exceptions propagate to test runner\n- ❌ Complex `setUp` that tests depend on implicitly — make dependencies explicit\n- ❌ Using `print()` for debugging — use `self.assertEqual` with descriptive messages\n- ❌ Tests that depend on execution order within a class\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4823,"content_sha256":"3b83687e4d7c9a6e4389722a517ad8aabb03c46fc9ea44860e37a2f0affbab2e"},{"filename":"reference/playbook.md","content":"# Python unittest — Advanced Playbook\n\n## §1 — Project Setup\n\n### Project Structure\n```\nproject/\n├── pyproject.toml\n├── src/\n│ └── myapp/\n│ ├── __init__.py\n│ ├── services/\n│ │ ├── __init__.py\n│ │ ├── user_service.py\n│ │ └── email_service.py\n│ ├── models/\n│ │ ├── __init__.py\n│ │ └── user.py\n│ └── utils/\n│ ├── __init__.py\n│ └── validators.py\n├── tests/\n│ ├── __init__.py\n│ ├── conftest.py\n│ ├── unit/\n│ │ ├── __init__.py\n│ │ ├── test_user_service.py\n│ │ ├── test_email_service.py\n│ │ └── test_validators.py\n│ ├── integration/\n│ │ ├── __init__.py\n│ │ └── test_api.py\n│ └── fixtures/\n│ ├── users.json\n│ └── responses/\n│ └── api_response.json\n└── .coveragerc\n```\n\n### pyproject.toml\n```toml\n[project]\nname = \"myapp\"\nversion = \"1.0.0\"\nrequires-python = \">=3.10\"\n\n[project.optional-dependencies]\ntest = [\n \"coverage>=7.4\",\n \"responses>=0.25\",\n \"freezegun>=1.4\",\n \"parameterized>=0.9\",\n]\n\n[tool.coverage.run]\nsource = [\"src/myapp\"]\nomit = [\"tests/*\", \"*/migrations/*\"]\nbranch = true\n\n[tool.coverage.report]\nfail_under = 80\nshow_missing = true\nexclude_lines = [\n \"pragma: no cover\",\n \"if __name__ == .__main__.\",\n \"raise NotImplementedError\",\n]\n```\n\n### .coveragerc\n```ini\n[run]\nsource = src/myapp\nbranch = true\n\n[report]\nfail_under = 80\nshow_missing = true\n```\n\n---\n\n## §2 — Core Test Patterns\n\n### Basic Test Structure\n```python\nimport unittest\nfrom unittest.mock import Mock, patch, MagicMock, PropertyMock\nfrom myapp.services.user_service import UserService\nfrom myapp.models.user import User\n\n\nclass TestUserService(unittest.TestCase):\n \"\"\"Test suite for UserService.\"\"\"\n\n def setUp(self):\n \"\"\"Set up test fixtures.\"\"\"\n self.mock_repo = Mock()\n self.mock_email = Mock()\n self.service = UserService(repo=self.mock_repo, email_service=self.mock_email)\n\n def tearDown(self):\n \"\"\"Clean up after tests.\"\"\"\n self.mock_repo.reset_mock()\n self.mock_email.reset_mock()\n\n # --- Creation Tests ---\n\n def test_create_user_returns_user_with_id(self):\n self.mock_repo.save.return_value = User(id=1, name=\"Alice\", email=\"[email protected]\")\n\n user = self.service.create(\"Alice\", \"[email protected]\")\n\n self.assertIsNotNone(user.id)\n self.assertEqual(\"Alice\", user.name)\n self.assertEqual(\"[email protected]\", user.email)\n self.mock_repo.save.assert_called_once()\n\n def test_create_user_sends_welcome_email(self):\n self.mock_repo.save.return_value = User(id=1, name=\"Alice\", email=\"[email protected]\")\n\n self.service.create(\"Alice\", \"[email protected]\")\n\n self.mock_email.send_welcome.assert_called_once_with(\"[email protected]\", \"Alice\")\n\n def test_create_user_with_invalid_email_raises(self):\n with self.assertRaises(ValueError) as ctx:\n self.service.create(\"Alice\", \"bad-email\")\n self.assertIn(\"invalid email\", str(ctx.exception).lower())\n\n def test_create_user_with_duplicate_email_raises(self):\n self.mock_repo.find_by_email.return_value = User(id=1, name=\"Existing\", email=\"[email protected]\")\n\n with self.assertRaises(ValueError) as ctx:\n self.service.create(\"Alice\", \"[email protected]\")\n self.assertIn(\"already exists\", str(ctx.exception).lower())\n\n # --- Retrieval Tests ---\n\n def test_find_returns_user_when_exists(self):\n expected = User(id=1, name=\"Alice\", email=\"[email protected]\")\n self.mock_repo.find.return_value = expected\n\n result = self.service.find(1)\n\n self.assertEqual(expected, result)\n self.mock_repo.find.assert_called_once_with(1)\n\n def test_find_returns_none_for_missing_user(self):\n self.mock_repo.find.return_value = None\n self.assertIsNone(self.service.find(999))\n\n # --- Collection Assertions ---\n\n def test_list_returns_all_users(self):\n users = [User(id=i, name=f\"User{i}\", email=f\"u{i}@test.com\") for i in range(3)]\n self.mock_repo.find_all.return_value = users\n\n result = self.service.list()\n\n self.assertEqual(3, len(result))\n self.assertIsInstance(result, list)\n\n def test_search_filters_by_name(self):\n users = [\n User(id=1, name=\"Alice Smith\", email=\"[email protected]\"),\n User(id=2, name=\"Bob Jones\", email=\"[email protected]\"),\n ]\n self.mock_repo.find_all.return_value = users\n\n result = self.service.search(\"Alice\")\n\n self.assertTrue(all(\"Alice\" in u.name for u in result))\n\n # --- Numeric Assertions ---\n\n def test_balance_calculation(self):\n self.assertAlmostEqual(10.05, self.service.calculate_balance(10.0, 0.05), places=2)\n\n def test_discount_within_range(self):\n discount = self.service.calculate_discount(100.0, \"VIP\")\n self.assertGreaterEqual(discount, 0)\n self.assertLessEqual(discount, 100)\n```\n\n---\n\n## §3 — Mocking & Patching\n\n### Patch Decorator & Context Manager\n```python\nclass TestExternalIntegration(unittest.TestCase):\n\n @patch(\"myapp.services.user_service.requests.get\")\n def test_fetch_external_user(self, mock_get):\n \"\"\"Patch at the location where it's used, not where it's defined.\"\"\"\n mock_get.return_value.status_code = 200\n mock_get.return_value.json.return_value = {\"name\": \"Alice\", \"id\": 42}\n\n result = self.service.fetch_external(42)\n\n self.assertEqual(\"Alice\", result[\"name\"])\n mock_get.assert_called_once_with(\"https://api.example.com/users/42\", timeout=10)\n\n @patch(\"myapp.services.user_service.requests.get\")\n def test_fetch_external_handles_timeout(self, mock_get):\n mock_get.side_effect = requests.Timeout(\"Connection timed out\")\n\n with self.assertRaises(ServiceUnavailableError):\n self.service.fetch_external(42)\n\n @patch(\"myapp.services.user_service.requests.get\")\n def test_fetch_external_retries_on_failure(self, mock_get):\n mock_get.side_effect = [\n requests.ConnectionError(\"failed\"),\n requests.ConnectionError(\"failed\"),\n Mock(status_code=200, json=Mock(return_value={\"name\": \"Alice\"})),\n ]\n\n result = self.service.fetch_external(42)\n\n self.assertEqual(\"Alice\", result[\"name\"])\n self.assertEqual(3, mock_get.call_count)\n\n def test_context_manager_patching(self):\n with patch(\"myapp.services.email_service.smtplib.SMTP\") as mock_smtp:\n instance = mock_smtp.return_value.__enter__.return_value\n instance.sendmail.return_value = {}\n\n service = EmailService()\n service.send(\"[email protected]\", \"Subject\", \"Body\")\n\n instance.sendmail.assert_called_once()\n\n @patch.multiple(\n \"myapp.services.user_service\",\n requests=Mock(),\n cache=Mock(),\n logger=Mock(),\n )\n def test_multiple_patches(self, **mocks):\n # All three are patched simultaneously\n pass\n```\n\n### Mock Advanced Patterns\n```python\nclass TestMockPatterns(unittest.TestCase):\n\n def test_mock_property(self):\n user = Mock()\n type(user).is_admin = PropertyMock(return_value=True)\n self.assertTrue(user.is_admin)\n\n def test_mock_spec(self):\n \"\"\"spec=True restricts mock to real class interface.\"\"\"\n mock_repo = Mock(spec=UserRepository)\n mock_repo.find(1) # OK - method exists\n with self.assertRaises(AttributeError):\n mock_repo.nonexistent_method() # Fails - not in spec\n\n def test_mock_side_effect_function(self):\n def dynamic_response(user_id):\n if user_id == 1:\n return User(id=1, name=\"Alice\")\n return None\n\n self.mock_repo.find.side_effect = dynamic_response\n self.assertIsNotNone(self.service.find(1))\n self.assertIsNone(self.service.find(999))\n\n def test_call_args_inspection(self):\n self.mock_repo.save(User(name=\"Alice\", email=\"[email protected]\"))\n self.mock_repo.save(User(name=\"Bob\", email=\"[email protected]\"))\n\n # Inspect all calls\n self.assertEqual(2, self.mock_repo.save.call_count)\n first_call_args = self.mock_repo.save.call_args_list[0]\n self.assertEqual(\"Alice\", first_call_args[0][0].name)\n\n def test_any_matcher(self):\n from unittest.mock import ANY\n\n self.service.create(\"Alice\", \"[email protected]\")\n self.mock_repo.save.assert_called_once_with(ANY)\n\n def test_mock_async(self):\n \"\"\"For async code testing.\"\"\"\n mock_client = Mock()\n mock_client.fetch = Mock(return_value=asyncio.coroutine(lambda: {\"data\": \"value\"})())\n # Or use AsyncMock in Python 3.8+\n mock_client.fetch = AsyncMock(return_value={\"data\": \"value\"})\n```\n\n---\n\n## §4 — SubTest & Parameterized\n\n### subTest for Data-Driven\n```python\nclass TestValidation(unittest.TestCase):\n\n def test_email_validation_multiple_cases(self):\n cases = [\n (\"[email protected]\", True, \"standard email\"),\n (\"[email protected]\", True, \"dotted local\"),\n (\"[email protected]\", True, \"plus tag\"),\n (\"[email protected]\", True, \"subdomain\"),\n (\"userexample.com\", False, \"missing @\"),\n (\"user@\", False, \"missing domain\"),\n (\"@example.com\", False, \"missing local\"),\n (\"\", False, \"empty string\"),\n (\" \", False, \"whitespace only\"),\n ]\n for email, expected, label in cases:\n with self.subTest(email=email, label=label):\n self.assertEqual(expected, is_valid_email(email))\n\n def test_password_strength(self):\n cases = {\n \"too short\": (\"abc\", False),\n \"no uppercase\": (\"password123!\", False),\n \"no number\": (\"Password!!!\", False),\n \"valid strong\": (\"P@ssw0rd!Long\", True),\n \"just minimum length\": (\"P@ssw0rd\", True),\n }\n for label, (password, expected) in cases.items():\n with self.subTest(label=label, password=password):\n self.assertEqual(expected, is_strong_password(password))\n\n def test_status_code_mapping(self):\n mappings = [\n (200, \"ok\"),\n (201, \"created\"),\n (400, \"bad_request\"),\n (404, \"not_found\"),\n (500, \"server_error\"),\n ]\n for code, expected_status in mappings:\n with self.subTest(code=code):\n self.assertEqual(expected_status, map_status_code(code))\n```\n\n### Using parameterized Library\n```python\nfrom parameterized import parameterized, parameterized_class\n\n\nclass TestCalculator(unittest.TestCase):\n\n @parameterized.expand([\n (\"add_positive\", 2, 3, 5),\n (\"add_negative\", -1, -1, -2),\n (\"add_zero\", 0, 0, 0),\n (\"add_mixed\", -1, 1, 0),\n ])\n def test_add(self, name, a, b, expected):\n self.assertEqual(expected, Calculator.add(a, b))\n\n @parameterized.expand([\n (\"divide_normal\", 10, 2, 5.0),\n (\"divide_fraction\", 1, 3, 0.333),\n ])\n def test_divide(self, name, a, b, expected):\n self.assertAlmostEqual(expected, Calculator.divide(a, b), places=3)\n\n @parameterized.expand([\n (\"divide_by_zero\",),\n ])\n def test_divide_by_zero(self, name):\n with self.assertRaises(ZeroDivisionError):\n Calculator.divide(1, 0)\n\n\n# Parameterized test class\n@parameterized_class([\n {\"browser\": \"chrome\", \"headless\": True},\n {\"browser\": \"firefox\", \"headless\": True},\n {\"browser\": \"edge\", \"headless\": False},\n])\nclass TestCrossBrowser(unittest.TestCase):\n browser: str\n headless: bool\n\n def test_homepage_loads(self):\n driver = create_driver(self.browser, self.headless)\n try:\n driver.get(\"http://localhost:3000\")\n self.assertIn(\"Welcome\", driver.title)\n finally:\n driver.quit()\n```\n\n---\n\n## §5 — Time & Environment Mocking\n\n### FreezeGun for Time\n```python\nfrom freezegun import freeze_time\nfrom datetime import datetime, timedelta\n\n\nclass TestTimeDependent(unittest.TestCase):\n\n @freeze_time(\"2025-01-15 10:00:00\")\n def test_trial_not_expired(self):\n user = User(trial_start=datetime(2025, 1, 1))\n self.assertFalse(user.is_trial_expired())\n\n @freeze_time(\"2025-02-15 10:00:00\")\n def test_trial_expired_after_30_days(self):\n user = User(trial_start=datetime(2025, 1, 1))\n self.assertTrue(user.is_trial_expired())\n\n def test_time_travel(self):\n with freeze_time(\"2025-01-01\") as frozen:\n user = User(trial_start=datetime.now())\n self.assertFalse(user.is_trial_expired())\n\n frozen.tick(timedelta(days=31))\n self.assertTrue(user.is_trial_expired())\n\n @freeze_time(\"2025-03-15 09:00:00\")\n def test_business_hours_check(self):\n self.assertTrue(is_business_hours()) # Saturday 9am\n\n @freeze_time(\"2025-03-15 22:00:00\")\n def test_after_hours(self):\n self.assertFalse(is_business_hours())\n```\n\n### Environment Variables\n```python\nclass TestConfiguration(unittest.TestCase):\n\n @patch.dict(\"os.environ\", {\"DATABASE_URL\": \"sqlite:///test.db\", \"DEBUG\": \"true\"})\n def test_uses_test_database(self):\n config = AppConfig.load()\n self.assertEqual(\"sqlite:///test.db\", config.database_url)\n self.assertTrue(config.debug)\n\n @patch.dict(\"os.environ\", {}, clear=True)\n def test_defaults_when_env_missing(self):\n config = AppConfig.load()\n self.assertEqual(\"sqlite:///default.db\", config.database_url)\n self.assertFalse(config.debug)\n\n @patch.dict(\"os.environ\", {\"API_KEY\": \"\"})\n def test_empty_api_key_raises(self):\n with self.assertRaises(ConfigurationError):\n AppConfig.load()\n```\n\n---\n\n## §6 — Custom TestCase & Mixins\n\n### Custom Base TestCase\n```python\nimport json\nimport os\n\n\nclass BaseTestCase(unittest.TestCase):\n \"\"\"Base test case with shared utilities.\"\"\"\n\n FIXTURES_DIR = os.path.join(os.path.dirname(__file__), \"fixtures\")\n\n def load_fixture(self, filename):\n path = os.path.join(self.FIXTURES_DIR, filename)\n with open(path) as f:\n if filename.endswith(\".json\"):\n return json.load(f)\n return f.read()\n\n def assertValidEmail(self, email):\n import re\n pattern = r'^[\\w+\\-.]+@[a-z\\d\\-]+(\\.[a-z\\d\\-]+)*\\.[a-z]+

Python unittest Skill Core Patterns Basic Test Assertions SubTest (Parameterized) Mocking Lifecycle Run: or Discover: Deep Patterns For advanced patterns, debugging guides, CI/CD integration, and best practices, see . ---

\n self.assertRegex(email, pattern, f\"'{email}' is not a valid email\")\n\n def assertDictSubset(self, subset, full_dict):\n \"\"\"Assert that subset is contained within full_dict.\"\"\"\n for key, value in subset.items():\n self.assertIn(key, full_dict, f\"Key '{key}' not found\")\n self.assertEqual(value, full_dict[key],\n f\"Value mismatch for key '{key}': {value} != {full_dict[key]}\")\n\n def assertEventually(self, condition_fn, timeout=5, interval=0.5, msg=None):\n \"\"\"Poll until condition is true or timeout.\"\"\"\n import time\n deadline = time.time() + timeout\n while time.time() \u003c deadline:\n if condition_fn():\n return\n time.sleep(interval)\n self.fail(msg or f\"Condition not met within {timeout}s\")\n\n def assertResponseOk(self, response):\n self.assertIn(response.status_code, range(200, 300),\n f\"Expected 2xx, got {response.status_code}: {response.text[:200]}\")\n```\n\n### Mixin for API Testing\n```python\nclass APITestMixin:\n \"\"\"Mixin for API test utilities.\"\"\"\n\n BASE_URL = \"http://localhost:3000/api\"\n\n def api_get(self, path, **kwargs):\n import requests\n return requests.get(f\"{self.BASE_URL}{path}\", **kwargs)\n\n def api_post(self, path, data=None, **kwargs):\n import requests\n return requests.post(f\"{self.BASE_URL}{path}\", json=data, **kwargs)\n\n def assertJsonResponse(self, response, expected_keys):\n self.assertEqual(\"application/json\", response.headers.get(\"Content-Type\"))\n data = response.json()\n for key in expected_keys:\n self.assertIn(key, data, f\"Missing key '{key}' in response\")\n\n\nclass TestUserAPI(BaseTestCase, APITestMixin):\n\n def test_create_user_endpoint(self):\n response = self.api_post(\"/users\", {\"name\": \"Alice\", \"email\": \"[email protected]\"})\n self.assertResponseOk(response)\n self.assertJsonResponse(response, [\"id\", \"name\", \"email\"])\n```\n\n---\n\n## §7 — CI/CD Integration\n\n### GitHub Actions\n```yaml\nname: Python unittest Tests\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n python-version: ['3.10', '3.11', '3.12']\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-python@v5\n with:\n python-version: ${{ matrix.python-version }}\n cache: pip\n\n - name: Install dependencies\n run: |\n pip install -e \".[test]\"\n\n - name: Run tests with coverage\n run: |\n python -m coverage run -m unittest discover -s tests -p \"test_*.py\" -v\n python -m coverage report --fail-under=80\n python -m coverage xml -o coverage.xml\n\n - name: Upload coverage\n if: always()\n uses: actions/upload-artifact@v4\n with:\n name: coverage-${{ matrix.python-version }}\n path: coverage.xml\n```\n\n### Running Tests\n```bash\n# Discover and run all tests\npython -m unittest discover -s tests -p \"test_*.py\" -v\n\n# Run specific test module\npython -m unittest tests.unit.test_user_service -v\n\n# Run specific test class\npython -m unittest tests.unit.test_user_service.TestUserService -v\n\n# Run specific test method\npython -m unittest tests.unit.test_user_service.TestUserService.test_create_user_returns_user_with_id\n\n# With coverage\ncoverage run -m unittest discover -s tests -v\ncoverage report --show-missing\ncoverage html # Generates htmlcov/index.html\n```\n\n---\n\n## §8 — Debugging Table\n\n| # | Problem | Cause | Fix |\n|---|---------|-------|-----|\n| 1 | Test method not discovered | Name doesn't start with `test_` | Rename to `test_xxx`; unittest only runs `test_*` methods |\n| 2 | `patch` doesn't work | Patching at definition site, not import site | Patch where it's used: `@patch(\"myapp.services.requests.get\")` |\n| 3 | `Mock` returns Mock for everything | No `spec` set; any attribute access succeeds | Use `Mock(spec=RealClass)` to restrict interface |\n| 4 | `setUp` changes not visible | Using class-level `setUpClass` but need per-test | Use instance-level `setUp` for per-test isolation |\n| 5 | `subTest` failures unclear | Missing label in subTest | Add `with self.subTest(label=label, input=x):` |\n| 6 | `assertRaises` doesn't catch | Exception type mismatch or not raised | Verify exact exception class; ensure code path triggers |\n| 7 | Coverage report shows 0% | Source path not configured | Set `source = [\"src/myapp\"]` in .coveragerc or pyproject.toml |\n| 8 | `patch.dict` doesn't restore | Used outside context manager | Use `with patch.dict(...)` or `@patch.dict` decorator |\n| 9 | Async test not awaited | Using `async def test_` without runner | Use `unittest.IsolatedAsyncioTestCase` for async tests |\n| 10 | `freeze_time` not affecting code | Code uses `time.time()` not `datetime.now()` | freezegun patches both; verify import path in target code |\n| 11 | Import error in test discovery | Circular import or missing `__init__.py` | Add `__init__.py` to all test directories; fix circular deps |\n| 12 | `MagicMock` vs `Mock` confusion | `MagicMock` supports magic methods, `Mock` doesn't | Use `MagicMock` when code uses `len()`, `iter()`, `bool()` etc. |\n\n---\n\n## §9 — Best Practices Checklist\n\n1. **`setUp`/`tearDown` for isolation** — each test gets fresh state; never rely on test order\n2. **Patch where imported, not defined** — `@patch(\"myapp.module.dependency\")` not `@patch(\"dep_lib.func\")`\n3. **`Mock(spec=Class)` always** — catches interface mismatches at test time\n4. **`subTest` for parameterized** — each case runs independently with clear failure messages\n5. **Custom assertions for domains** — `assertValidEmail()`, `assertResponseOk()` improve readability\n6. **`assertRaises` as context manager** — access exception via `ctx.exception` for message checks\n7. **Fixture files over inline data** — `tests/fixtures/` for JSON/YAML test data\n8. **Coverage gating in CI** — `--fail-under=80` prevents coverage regression\n9. **`freezegun` for time-dependent** — deterministic time testing without sleep\n10. **`patch.dict` for env vars** — isolated environment variable testing\n11. **Base TestCase for reuse** — shared helpers, fixtures, custom assertions in base class\n12. **Separate unit/integration dirs** — run unit tests fast; integration tests with services\n13. **`AsyncMock` for async code** — Python 3.8+ `unittest.mock.AsyncMock` for coroutines\n14. **Consider pytest migration** — for new projects, pytest offers fixtures, markers, and plugins\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":21209,"content_sha256":"c422c114513415e980a612d6b75ac743127f23ba62265ae0c94e97ad1da79ce6"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Python unittest Skill","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Basic Test","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import unittest\n\nclass TestCalculator(unittest.TestCase):\n def setUp(self):\n self.calc = Calculator()\n\n def test_add(self):\n self.assertEqual(self.calc.add(2, 3), 5)\n\n def test_divide_by_zero(self):\n with self.assertRaises(ZeroDivisionError):\n self.calc.divide(10, 0)\n\n def test_multiple_assertions(self):\n self.assertEqual(self.calc.add(2, 2), 4)\n self.assertEqual(self.calc.subtract(5, 3), 2)\n self.assertAlmostEqual(self.calc.divide(10, 3), 3.333, places=3)\n\n def tearDown(self):\n pass # cleanup\n\nif __name__ == '__main__':\n unittest.main()","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Assertions","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"self.assertEqual(a, b)\nself.assertNotEqual(a, b)\nself.assertTrue(condition)\nself.assertFalse(condition)\nself.assertIsNone(obj)\nself.assertIsNotNone(obj)\nself.assertIs(a, b) # same object\nself.assertIn(item, collection)\nself.assertNotIn(item, collection)\nself.assertIsInstance(obj, cls)\nself.assertAlmostEqual(a, b, places=5)\nself.assertGreater(a, b)\nself.assertLess(a, b)\nself.assertRegex(str, r'\\d+')\nself.assertCountEqual(a, b) # same elements, any order\n\n# Exception\nwith self.assertRaises(ValueError) as ctx:\n raise ValueError(\"bad\")\nself.assertIn(\"bad\", str(ctx.exception))\n\n# Warning\nwith self.assertWarns(DeprecationWarning):\n deprecated_function()","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"SubTest (Parameterized)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def test_add_multiple(self):\n test_cases = [(2, 3, 5), (-1, 1, 0), (0, 0, 0)]\n for a, b, expected in test_cases:\n with self.subTest(a=a, b=b):\n self.assertEqual(self.calc.add(a, b), expected)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Mocking","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from unittest.mock import patch, MagicMock, Mock\n\nclass TestUserService(unittest.TestCase):\n @patch('myapp.service.UserRepository')\n @patch('myapp.service.EmailService')\n def test_create_user(self, MockEmail, MockRepo):\n mock_repo = MockRepo.return_value\n mock_repo.save.return_value = User(1, 'Alice')\n\n service = UserService()\n result = service.create_user('[email protected]', 'Alice')\n\n self.assertEqual(result.id, 1)\n mock_repo.save.assert_called_once()\n MockEmail.return_value.send_welcome.assert_called_with('[email protected]')","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Lifecycle","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"setUpClass() → Once before all tests (classmethod)\nsetUp() → Before each test\ntest_method() → Test\ntearDown() → After each test\ntearDownClass() → Once after all tests (classmethod)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Run: ","type":"text"},{"text":"python -m unittest","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"python -m unittest test_module.TestClass.test_method","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Discover: ","type":"text"},{"text":"python -m unittest discover -s tests -p \"test_*.py\"","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Deep Patterns","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"For advanced patterns, debugging guides, CI/CD integration, and best practices, see ","type":"text"},{"text":"reference/playbook.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"unittest-skill","author":"@skillopedia","source":{"stars":302,"repo_name":"agent-skills","origin_url":"https://github.com/lambdatest/agent-skills/blob/HEAD/unittest-skill/SKILL.md","repo_owner":"lambdatest","body_sha256":"1a66bb6e2e9b0a23f725803a7b589c3f1838ebe9406b980cdc27b1db664a337d","cluster_key":"b2a502371849b007fe8c32a630d010f7029e6dd42dac69c3856469874dbcf13d","clean_bundle":{"format":"clean-skill-bundle-v1","source":"lambdatest/agent-skills/unittest-skill/SKILL.md","attachments":[{"id":"238dc94c-222a-50a0-8514-05df5bdde8de","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/238dc94c-222a-50a0-8514-05df5bdde8de/attachment.md","path":"reference/advanced-patterns.md","size":4823,"sha256":"3b83687e4d7c9a6e4389722a517ad8aabb03c46fc9ea44860e37a2f0affbab2e","contentType":"text/markdown; charset=utf-8"},{"id":"19f2ffbc-e6f2-5b54-8709-a0cee3d304f8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/19f2ffbc-e6f2-5b54-8709-a0cee3d304f8/attachment.md","path":"reference/playbook.md","size":21209,"sha256":"c422c114513415e980a612d6b75ac743127f23ba62265ae0c94e97ad1da79ce6","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"e0cf7f5730f898ad4e50e5efd8625094278944cea19411e1d43f9ede878cba28","attachment_count":2,"text_attachments":2,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"unittest-skill/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"testing-qa","metadata":{"author":"TestMu AI","version":"1.0"},"languages":["Python"],"import_tag":"clean-skills-v1","description":"Generates Python unittest tests. Built-in testing framework with TestCase, setUp/tearDown, and assertion methods. Use when user mentions \"unittest\", \"TestCase\", \"self.assertEqual\", \"Python unittest\". Triggers on: \"unittest\", \"TestCase\", \"self.assertEqual\", \"Python unittest\" (not pytest).\n"}},"renderedAt":1782981405976}

Python unittest Skill Core Patterns Basic Test Assertions SubTest (Parameterized) Mocking Lifecycle Run: or Discover: Deep Patterns For advanced patterns, debugging guides, CI/CD integration, and best practices, see . ---