unit_testing.md (3503B)
1 # Unit Testing 2 3 ## Writing Isolated and Focused Unit Tests 4 5 Unit tests should be **isolated and focused**, testing one small, well-defined 6 unit of functionality at a time. Each test should verify a single behavior 7 without relying on other parts of the system. When tests are too broad or 8 tightly coupled across components, they become brittle — a minor change in one 9 area can cause unrelated tests to fail. 10 11 To ensure reliability and maintainability, unit tests should: 12 13 - Run independently of external systems or global state. 14 - Use **mocks or stubs** to replace dependencies. 15 - Follow the 16 [**Arrange–Act–Assert**](/test_driven_development/arrange_act_assert.md) 17 pattern for clarity. 18 - Be **deterministic** and **fast** so they can run often during development. 19 20 Writing tests this way builds confidence in each unit, simplifies debugging, and 21 supports modular, testable code design. 22 23 --- 24 25 ## Enabling CI/CD Test Environments with Isolated Resources 26 27 For continuous integration workflows such as **GitHub Actions** or **Forgejo**, 28 unit tests should run within a fully isolated and reproducible environment. This 29 means the application should be capable of **spinning up a dedicated test 30 PostgreSQL instance** and a **test server API** that operates on a separate port 31 with its own **test-specific environment variables**. 32 33 These configurations ensure that test runs do not interfere with production or 34 staging databases. The test infrastructure should start up quickly and shut down 35 cleanly as part of the CI/CD pipeline, allowing automated workflows to execute 36 the full test suite independently for every build or pull request. This approach 37 guarantees repeatable, safe testing while maintaining complete separation 38 between test and production systems. 39 40 --- 41 42 ### Question: What's the difference between intergation tests and unit tests? 43 44 Unit tests focus on small, isolated pieces of code and run very fast, so they 45 give precise, quick feedback and make it easy to iterate or refactor without 46 breaking unrelated behavior. In contrast, integration tests exercise multiple 47 components together (like API, database, and services) to verify real workflows, 48 which provides higher confidence that the system behaves correctly as a whole. 49 50 The tradeoff is that integration tests are slower, more complex to set up, and 51 failures can be harder to diagnose, since a small change in one part of the 52 system can break a test somewhere else. Because of this, you typically rely on 53 unit tests for rapid development of specific functionality, and use integration 54 tests more sparingly to ensure that the integrated application still works end 55 to end. 56 57 --- 58 59 ## Abstractions and Patterns to Keep Tests Stable 60 61 This is where **interfaces** (traits), the facade and adapter patterns, 62 dependency injection, and internal mutability come in. When a piece of code is 63 wrapped behind a stable trait or adapter interface, the call sites that depend 64 on it can remain unchanged, while other developers are free to change the 65 internals of the wrapped functions without breaking those callers. This gives 66 more confidence in integration testing when integrating with different external 67 systems, call sites, or frontends, because the surface contracts stay stable 68 even as implementations evolve. These abstraction layers reinforce separation of 69 concerns between core functions, business logic, infrastructure, and UI/API 70 layers, so each layer can be unit-tested in isolation and then composed 71 predictably in integration tests.