{"post":{"slug":"mocking-explained-2026","title":"Mocking explained in 2026: when to mock, when to fake, when to stop","excerpt":"Mocking is both the most useful and most abused technique in testing. This 2026 guide explains mocks vs fakes vs stubs, and when each belongs in your test suite.","category":"code","date":"April 20, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">M</span>ocking is the technique that makes unit testing possible: instead of hitting a real database or calling a real payment API in every test, you replace the dependency with a controlled stand-in. The trouble is that the word \"mock\" is used for three distinct things — mocks, stubs, and fakes — each with different trade-offs. Using the wrong one is the most common reason test suites become fragile and meaningless.\n\n## What changed in 2026\n\n- **Vitest's `vi.mock()` and `vi.spyOn()`** became the standard for JavaScript, replacing Jest in most new projects while keeping a compatible API.\n- **Python's `unittest.mock`** remains the standard; `pytest-mock` wraps it with a cleaner fixture API.\n- **Over-mocking backlash grew.** The community converged on \"mock at the boundary\" and \"prefer fakes\" after years of brittle mock-heavy test suites.\n- **TypeScript strict types** mean mock objects that do not fully implement an interface fail at compile time — caught earlier.\n\n## The three test doubles\n\n| Name | What it does | When to use |\n| --- | --- | --- |\n| **Stub** | Returns canned data; no assertions | \"Give me a user\" — you just need the value |\n| **Mock** | Records calls; verifies interactions | \"Assert that `sendEmail` was called once\" |\n| **Fake** | Working lightweight implementation | Complex collaborators used in many tests |\n\n```ts\n// Stub — returns hardcoded data\nconst userRepo = { findById: async () => ({ id: \"1\", name: \"Alice\" }) };\n\n// Mock — verifies a call happened\nconst emailService = { send: vi.fn() };\n// later: expect(emailService.send).toHaveBeenCalledOnce();\n\n// Fake — real logic, no real I/O\nclass InMemoryUserRepository implements UserRepository {\n  private store = new Map<string, User>();\n  async findById(id: string) { return this.store.get(id) ?? null; }\n  async save(user: User) { this.store.set(user.id, user); }\n}\n```\n\n## Mock at the boundary\n\nThe golden rule: mock the line where your code meets the outside world.\n\n```\nYour code  →  [boundary]  →  Outside world\n               ↑ mock here\n```\n\nBoundary examples: HTTP clients, email senders, payment gateways, file system, clocks/timers. Non-boundary examples: your own service classes, domain objects, pure functions.\n\nIf you mock your own `OrderService` to test `CheckoutService`, you are testing a mock's behaviour, not real integration. Use a real `OrderService` (or a fake) in that test.\n\n## When to use each\n\n```ts\n// Use a STUB when the test doesn't care about the call, just the return value\nit(\"formats the user name\", async () => {\n  const repo = { findById: async () => ({ id: \"1\", name: \"alice\" }) };\n  const svc = new UserService(repo);\n  expect(await svc.displayName(\"1\")).toBe(\"Alice\");\n});\n\n// Use a MOCK when you need to assert the call happened\nit(\"sends a welcome email on signup\", async () => {\n  const emailer = { send: vi.fn() };\n  const svc = new AuthService(emailer);\n  await svc.signup(\"a@example.com\");\n  expect(emailer.send).toHaveBeenCalledWith(\n    expect.objectContaining({ to: \"a@example.com\", template: \"welcome\" })\n  );\n});\n\n// Use a FAKE when the collaborator is complex and used in many tests\nconst repo = new InMemoryUserRepository();\n```\n\n## Python example\n\n```python\nfrom unittest.mock import MagicMock, call\nfrom myapp.services import NotificationService\n\ndef test_notify_sends_sms_and_email():\n    sms = MagicMock()\n    mail = MagicMock()\n    svc = NotificationService(sms_client=sms, mail_client=mail)\n\n    svc.notify(user_id=\"u1\", message=\"Hello\")\n\n    sms.send.assert_called_once_with(user_id=\"u1\", text=\"Hello\")\n    mail.send.assert_called_once()\n```\n\n## How to pick\n\n1. **External service call** (HTTP, email, SMS, payment) — mock or stub at the client boundary.\n2. **Clock / random number** — replace with a controllable fake (clock that you advance manually).\n3. **Complex stateful collaborator used across many tests** — write an in-memory fake.\n4. **Simple one-off data return** — use a plain stub object.\n5. **Database access in unit tests** — use an in-memory fake repository.\n6. **Database access in integration tests** — use a real database (Testcontainers).\n\n## Common mistakes\n\n**Mocking your own internal classes.** When `ServiceA` is tested by mocking `ServiceB`, you miss every bug in their interaction.\n\n**Verifying implementation details.** Asserting `privateHelper` was called three times. When you inline that helper, the test breaks despite no behaviour change.\n\n**Over-specifying mock calls.** Asserting the exact arguments including defaults and timestamps that change. Use `expect.objectContaining` and `expect.any(Date)`.\n\n**Forgetting to reset mocks.** Mock state that bleeds between tests causes order-dependent failures. Use `vi.clearAllMocks()` in `beforeEach`, or `autoClearMocks: true` in config.\n\n## What to skip\n\n- **Mocking the database in integration tests** — defeats their purpose. See [Integration testing explained in 2026](/blog/integration-testing-explained-2026).\n- **Mocking pure functions** — they have no side effects; just call them.\n- **Complex mock setup that mirrors the real implementation** — that is a sign you need a real fake or a real dependency.\n\n## FAQ\n\n**What is the difference between a spy and a mock?**\nA spy wraps the real implementation and records calls; a mock replaces it entirely. `vi.spyOn` creates a spy; `vi.fn()` creates a mock.\n\n**Should I use an auto-mocking library?**\nAuto-mockers generate mock objects from interfaces automatically. They are convenient for large interfaces but can generate over-specified tests. Use them where they save real time.\n\n**How do I mock a module import?**\nVitest: `vi.mock('./module')`. Jest: `jest.mock('./module')`. Python: `unittest.mock.patch('myapp.module.ClassName')`.\n\n**When is over-mocking a sign of bad design?**\nWhen you need 8 mocks to test one method, the method has too many dependencies. Consider splitting responsibilities.\n\n## Where to go next\n\nSee [Unit testing explained in 2026](/blog/unit-testing-explained-2026), [Integration testing explained in 2026](/blog/integration-testing-explained-2026), and [Dependency injection explained in 2026](/blog/dependency-injection-explained-2026).","hue":306,"metaTitle":"Mocking Explained 2026","metaDescription":"Complete 2026 guide to mocking in tests. The difference between mocks, stubs, and fakes, when each belongs in your suite","keywords":["mocking explained 2026","mock vs stub vs fake","how to mock in javascript 2026","jest mock 2026","python unittest mock","test doubles 2026","over-mocking anti-pattern","in-memory repository fake","mocking best practices","how to test without a database 2026"],"tldr":["**A mock verifies interactions; a stub returns canned data; a fake is a working lightweight implementation** — most developers conflate all three as \"mocks.\"","**Mock external dependencies (HTTP, email, payment), not your own code** — over-mocking creates tests that test nothing real.","**Prefer fakes over mocks for complex collaborators** — an in-memory repository is more reliable than a mock with 10 `.when()` chains.","**Mock at the boundary of your system**, not deep inside it — infrastructure adapters, not service classes.","**Skip:** mocking the database in integration tests — use a real database with Testcontainers instead."],"readTime":"4 min read","wordCount":879},"related":[{"slug":"what-is-mocking-in-tests-2026","title":"What Is Mocking in Tests? 2026 Guide","excerpt":"Mocking replaces a real dependency with a fake object that records how it was called, so a test can verify behavior without touching a real database or network. Here is how mocking works in 2026.","category":"code","date":"July 23, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":280},{"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","readTime":"5 min read","hue":319},{"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}],"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":"midjourney-vs-dalle-2026","title":"Midjourney vs DALL-E in 2026: honest comparison for real creative work","excerpt":"Midjourney and DALL-E 3 take very different approaches to AI image generation. Here is which one wins for illustration, product shots, and creative workflows in 2026.","category":"ai","date":"April 20, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":212},"older":{"slug":"monolith-vs-microservices-2026","title":"Monolith vs microservices in 2026: which architecture is right?","excerpt":"The microservices hype has cooled and the modular monolith has made a comeback. This 2026 guide gives you a concrete framework for choosing between them.","category":"code","date":"April 20, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":308}}