Skip to content

Testing Overview

Automated testing gives you confidence that your code does what it’s supposed to do — and keeps doing it as the codebase grows.

/\
/E2E\ ← Few, slow, expensive
/------\
/ Integ \ ← Some
/----------\
/ Unit \ ← Many, fast, cheap
/______________\
LevelScopeSpeedCostQuantity
UnitSingle function / class< 1msLowMany
IntegrationMultiple components togetherSecondsMediumSome
End-to-EndFull user journeyMinutesHighFew

Test a single unit of code in isolation. External dependencies (databases, APIs, time) are mocked.

  • Fast — run in milliseconds
  • Precise — pinpoint exactly what broke
  • Fragile to refactoring if testing implementation not behaviour

Test multiple components working together — e.g., a service + database, or an API endpoint + controller + database layer.

  • Slower than unit tests
  • Catch issues that unit tests miss (misconfigured ORM, wrong SQL, mismatched interfaces)
  • More maintenance overhead

Drive the application like a real user — browser automation (Playwright, Cypress) or API calls that hit the live system.

  • Most confidence — tests what users actually experience
  • Slowest to run, most brittle
  • Expensive to write and maintain

AAA (Arrange, Act, Assert):

Arrange — set up the system under test and its dependencies
Act — call the thing being tested
Assert — verify the expected outcome

FIRST:

  • Fast — unit tests should run in milliseconds
  • Independent — tests should not depend on each other or run order
  • Repeatable — same result every time, regardless of environment
  • Self-validating — pass or fail without manual inspection
  • Timely — written alongside (or before) the code

Test behaviour, not implementation: Test what a function does, not how it does it internally. Tests that check internal state break when you refactor.

# Bad test — checks internal state, too tightly coupled
def test_add_to_cart():
cart = ShoppingCart()
cart.add("item-1", qty=2)
assert cart._items["item-1"] == 2 # internal structure
# Good test — checks observable behaviour
def test_add_to_cart():
cart = ShoppingCart()
cart.add("item-1", qty=2)
assert cart.total_items() == 2
assert cart.contains("item-1")

Coverage measures what % of your code is executed by tests. It’s a useful signal but not a goal in itself:

  • 100% coverage doesn’t mean no bugs
  • Low coverage is a red flag
  • Aim for high coverage on business logic, less on boilerplate
Terminal window
# JavaScript (Jest)
npx jest --coverage
# Python (pytest)
pytest --cov=src --cov-report=html
# C# (.NET)
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov
LanguageUnit TestingMockingE2E
JavaScript/TypeScriptJest, VitestJest mocks, MSWPlaywright, Cypress
Pythonpytestpytest-mock, unittest.mockPlaywright
C#xUnit, NUnit, MSTestMoq, NSubstitutePlaywright
Gotesting (stdlib)testifyPlaywright
JavaJUnitMockitoPlaywright, Selenium