{"post":{"slug":"how-to-write-unit-tests-2026","title":"How to write unit tests in 2026: patterns, coverage, and what to actually test","excerpt":"Unit tests are fast, focused, and the foundation of a reliable test suite. This 2026 guide covers what to test, how to structure tests, and the patterns that make tests useful rather than just green.","category":"code","date":"March 2, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">U</span>nit tests are the fastest feedback loop in software development — milliseconds per test, no external services, no shared state. A good unit test suite catches regressions instantly, documents expected behavior, and makes refactoring safe. A bad unit test suite breaks on every change, gives false confidence, and becomes a maintenance burden. The difference is almost entirely in what you choose to test and how.\n\n## What changed in 2026\n\n- **AI-assisted test generation** (GitHub Copilot, Cursor) writes the boilerplate fast, but the test cases it generates often assert on implementation rather than behavior — developers need to review and improve them, not just accept.\n- **Vitest replaced Jest** as the dominant JavaScript unit test runner — it is faster, has native ESM support, and shares configuration with Vite.\n- **pytest remained dominant in Python** with richer plugin support; `pytest-asyncio` and `anyio` made testing async code straightforward.\n- **Property-based testing** (Hypothesis for Python, fast-check for JS) moved from niche to a recommended complement to example-based tests for complex logic.\n\n## The core principle: test behavior\n\nA unit test should answer: \"given this input, does the function produce the correct output or trigger the correct side effect?\"\n\n```python\n# Bad: tests implementation details\ndef test_process_order_calls_helper():\n    order = Order(items=[...])\n    with patch.object(order, \"_calculate_subtotal\") as mock:\n        order.process()\n    mock.assert_called_once()  # breaks if you rename the method\n\n# Good: tests observable behavior\ndef test_process_order_returns_correct_total():\n    order = Order(items=[Item(price=10, qty=2), Item(price=5, qty=1)])\n    result = order.process()\n    assert result.total == 25\n```\n\nIf the test breaks when you rename an internal method, it is testing implementation — that is a smell.\n\n## The Arrange-Act-Assert structure\n\nEvery unit test has three phases:\n\n```python\ndef test_transfer_reduces_sender_balance():\n    # Arrange\n    sender = Account(balance=1000)\n    receiver = Account(balance=200)\n\n    # Act\n    transfer(sender, receiver, amount=300)\n\n    # Assert\n    assert sender.balance == 700\n```\n\nOne assertion per test is a useful discipline — when a test fails, you know exactly which expectation was violated. More than one assertion is acceptable when they form a single logical check (e.g., asserting multiple fields of a returned object), but avoid testing two unrelated behaviors in one test.\n\n## What to mock\n\nMock at the **boundary** of the unit under test — the interfaces that connect it to the outside world:\n\n```python\nfrom unittest.mock import MagicMock, patch\n\ndef test_send_welcome_email_on_signup(mock_email_client):\n    email_client = MagicMock()\n    user_service = UserService(email_client=email_client)\n\n    user_service.register(email=\"user@example.com\", password=\"secret\")\n\n    email_client.send.assert_called_once_with(\n        to=\"user@example.com\",\n        template=\"welcome\",\n    )\n```\n\n**Mock:** HTTP clients, database sessions, file I/O, clocks, random number generators.\n**Do not mock:** the code under test, utility functions within the same module, data classes.\n\nInjecting dependencies (via constructor or function parameter) makes mocking easy. Global state and direct imports make it painful.\n\n## JavaScript example with Vitest\n\n```typescript\n// src/pricing.ts\nexport function applyDiscount(price: number, coupon: string): number {\n  if (coupon === \"SAVE10\") return price * 0.9;\n  if (coupon === \"SAVE20\") return price * 0.8;\n  return price;\n}\n\n// src/pricing.test.ts\nimport { describe, it, expect } from \"vitest\";\nimport { applyDiscount } from \"./pricing\";\n\ndescribe(\"applyDiscount\", () => {\n  it(\"applies 10% for SAVE10\", () => {\n    expect(applyDiscount(100, \"SAVE10\")).toBe(90);\n  });\n\n  it(\"applies 20% for SAVE20\", () => {\n    expect(applyDiscount(100, \"SAVE20\")).toBe(80);\n  });\n\n  it(\"returns full price for unknown coupon\", () => {\n    expect(applyDiscount(100, \"FAKE\")).toBe(100);\n  });\n});\n```\n\nCover the happy path, edge cases, and invalid inputs. Three tests for a three-branch function is the right starting point.\n\n## Coverage: what to aim for\n\n| Code type | Coverage goal |\n| --- | --- |\n| Core business logic (pricing, rules, calculations) | 90–100% |\n| Service/orchestration layer | 80–90% |\n| Utilities and helpers | 80%+ |\n| Controllers / route handlers | 60–80% (lean on integration tests) |\n| Generated code, migrations | Skip |\n| Boilerplate getters/setters | Skip |\n\nCoverage is a floor, not a ceiling. 100% coverage does not mean the code is correct — it means every line ran. A function that returns `None` instead of raising on bad input can have 100% coverage and wrong behavior.\n\n## How to pick what to test first\n\n1. **Business logic with branching?** High priority — every branch is a potential bug.\n2. **Input validation and edge cases?** High priority — these are the failure modes users encounter.\n3. **Pure functions (no side effects)?** Easy to test, high ROI; do them first.\n4. **Functions with complex mocking requirements?** Consider refactoring to separate concerns first — hard-to-test code is usually poorly structured.\n5. **Framework glue code (routing, ORM queries)?** Low priority for unit tests; cover with integration tests.\n\n## Common mistakes\n\n**Testing that mocks were called instead of testing outcomes.** If your test only checks that `send_email` was called, you learn nothing about whether the email content is correct. Verify the call arguments.\n\n**One giant test file per module.** Group tests by behavior, not by file. `test_checkout_with_valid_card`, `test_checkout_with_expired_card` is better than one `test_checkout` with a dozen assertions.\n\n**Not running tests on save.** Vitest and pytest both support watch mode. Tight feedback loops catch issues immediately.\n\n**Brittle date/time assertions.** Do not assert `result.created_at == datetime.now()` — the clock moves. Inject a clock mock or assert within a tolerance.\n\n**Slow unit tests.** If a unit test takes more than 100ms, it is probably doing real I/O. Find and mock the slow dependency.\n\n## What to skip\n\n- **Testing third-party libraries** — they have their own tests. Test how your code uses them.\n- **100% coverage as a hard requirement** — it shifts effort from high-value tests to testing trivial code.\n- **AI-generated tests you have not read** — they may assert on the wrong thing, giving false confidence.\n\n## FAQ\n\n**What is the difference between unit and integration tests?**\nUnit tests test a single unit in isolation, with dependencies mocked. Integration tests test multiple units working together, often against real or in-process databases.\n\n**Should I write tests before or after code?**\nTDD (test-first) forces you to think about the interface before the implementation, often leading to better design. Writing tests after works too — the important thing is that they exist and are maintained.\n\n**How many unit tests should a function have?**\nAt minimum: one per code path (one per branch). Also add tests for boundary values and invalid inputs. A function with 3 if-branches needs at least 3–5 tests.\n\n**What is property-based testing?**\nInstead of handwriting specific inputs, you define invariants (\"the output is always non-negative\") and a framework like Hypothesis generates hundreds of random inputs to try to falsify them. Excellent for finding edge cases in parsing, calculations, and transformations.\n\n## Where to go next\n\n- [How to write integration tests in 2026](/blog/how-to-write-integration-tests-2026)\n- [Mocking explained in 2026](/blog/mocking-explained-2026)\n- [How to handle errors gracefully in 2026](/blog/how-to-handle-errors-gracefully-2026)","hue":319,"metaTitle":"How to Write Unit Tests 2026: Patterns and Best Practices","metaDescription":"Write effective unit tests in 2026: Arrange-Act-Assert structure, what to mock, coverage goals, and the patterns that make tests maintainable and useful.","keywords":["how to write unit tests 2026","unit testing best practices","arrange act assert pattern","what to mock in unit tests","unit test coverage goals","unit testing python 2026","unit testing javascript 2026","test driven development 2026","unit vs integration tests","writing good unit tests"],"tldr":["**Test behavior, not implementation** — tests that assert on internal state or private methods break on every refactor; tests that assert on outputs and side effects survive.","**Arrange-Act-Assert is the universal structure** — set up the context, call the unit, assert the result; one assertion per test keeps failures diagnostic.","**Mock at the boundary, not inside** — mock HTTP clients, databases, and file systems; never mock the code you are testing.","**Aim for high coverage on business logic, not on boilerplate** — 100% coverage on getters and constructors is vanity; 90%+ on decision logic is valuable.","**Skip:** testing framework internals, generated code, and trivial one-liners — the value of a test is in documenting and protecting real behavior."],"readTime":"5 min read","wordCount":1093},"related":[{"slug":"unit-testing-explained-2026","title":"Unit testing explained in 2026: write tests that actually help","excerpt":"Most developers know they should write unit tests but struggle with what to test, how to structure them, and why their tests keep breaking. Here is the 2026 guide.","category":"code","date":"March 19, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":322},{"slug":"what-is-a-unit-test-2026","title":"What is a unit test in 2026? Testing the smallest pieces of code","excerpt":"A unit test checks that one small piece of code does what it should, in isolation. This guide explains what unit tests are, how they work, and how to write useful ones in 2026.","category":"code","date":"May 19, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":145},{"slug":"how-to-write-tests-2026","title":"How to Write Tests in 2026: a practical starter guide","excerpt":"Tests let you change code without fear. This 2026 guide covers what to test, the arrange-act-assert pattern, and how to start without testing everything.","category":"code","date":"June 16, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":195}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-write-integration-tests-2026","title":"How to write integration tests in 2026","excerpt":"Integration tests catch the bugs unit tests miss by exercising real component boundaries. Here is a practical 2026 guide with patterns that actually ship.","category":"code","date":"March 2, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305},"older":{"slug":"hsa-investing-strategy-2026","title":"HSA investing strategy in 2026: the most underrated retirement account","excerpt":"The HSA is the only triple-tax-advantaged account in America — and most people waste it as a checking account. Here is how to actually use it in 2026.","category":"finance","date":"March 2, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":296}}