Testing as living documentation

5 min read
fix me

Photo by vledov on Unsplash

In fast-paced, high-turnover environments, documentation needs to catch up to the code. Developers make changes but often forget to update the docs, which leads to outdated or irrelevant information. This creates a maintenance burden and can overwhelm new team members, making onboarding harder than it needs to be.

Instead of relying heavily on traditional documentation, a more sustainable approach is to treat tests as living documentation. By writing descriptive, user-focused tests, you can reduce the burden of maintaining documentation while ensuring that the code remains understandable, even as it evolves.

The Downside of heavy documentation

Outdated information

In fast-moving environments, documentation quickly becomes stale. Developers might forget to update the docs or simply lack the time to keep them current. This results in outdated information, which can mislead new developers and waste valuable time.

Maintenance burden

Maintaining extensive documentation takes a lot of time and effort. Writing it is one challenge, but keeping it updated as the code evolves is even harder. Over time, documentation becomes a lower priority, and before you know it, it’s out of sync with the actual code.

Overload and ignorance

When there’s too much documentation, it’s easy for important details to get buried. New developers might skim over critical information or even ignore the documentation entirely. Instead, many developers jump straight into the code, relying on what’s there to understand the system.

Testing as living documentation

In an environment with a large, constantly evolving codebase and frequent turnover, writing tests that focus on user behavior rather than internal implementation offers a more sustainable way to document your system.

Behavior-driven tests act as both functional tests and a source of up-to-date documentation for the codebase. They provide insight into how the system should behave from a user’s perspective, which is the most critical aspect of any application.

Example: Add to cart button on an e-commerce site

Let’s consider a common example: adding an “Add to Cart” button on an e-commerce site.

Expected behavior: When a user clicks the "Add to Cart" button, they should receive a confirmation that the product was successfully added to their cart, either through a notification, a cart counter update, or a visual change.

Here’s a type of test that focuses on implementation details instead of user behavior, in other words it tries to explain what the code does which can be a consequence of a complex implementation:

// Implementation test: Testing the internal function
import { render, fireEvent } from '@testing-library/react';
import AddToCartButton from './AddToCartButton';

test('calls handleAddToCart when the button is clicked', () => {
  const handleAddToCart = jest.fn();
  const { getByText } = render(<AddToCartButton onClick={handleAddToCart} />);
  fireEvent.click(getByText('Add to Cart'));

  expect(handleAddToCart).toHaveBeenCalledTimes(1);
});

This test checks whether an internal function, handleAddToCart, is called when the button is clicked. While this confirms that the function is triggered, it doesn’t verify the user experience, which is what truly matters.

Behavioural test: focus on user behavior

A better test focuses on what the user experiences after clicking the button, such as seeing a confirmation message or an updated cart counter.

// Behavioural test: Focusing on the user’s experience
import { render, fireEvent, screen } from '@testing-library/react';
import AddToCartButton from './AddToCartButton';

test('displays confirmation message after adding item to the cart', () => {
  render(<AddToCartButton />);

  fireEvent.click(screen.getByText('Add to Cart'));

  // Check that a confirmation message is shown
  expect(screen.getByText('Item added to your cart!')).toBeInTheDocument();
});

In this test we do two important things:

We simulate the user’s action (clicking the "Add to Cart" button).

We verify the expected behavior: the user sees a confirmation message or an updated cart counter.

This test is much more resilient to change because it focuses on what the user sees, not on the internal logic. Even if the internal implementation changes, the test will still pass as long as the user-facing behavior remains the same.

Why tests over documentation?

In high-turnover environments, testing serves as living documentation—always in sync with the code because tests will fail if they’re out of date. Here’s why tests provide more long-term value than documentation alone:

Instant Feedback: Tests provide immediate feedback when something breaks, unlike documentation that can easily be ignored or forgotten.

Encourages Refactoring: With solid test coverage, developers feel more confident making changes and refactoring code, knowing that the tests will catch potential issues.

Faster Onboarding: New hires can run tests to understand the codebase quickly. This reduces the reliance on documentation and speeds up the onboarding process.

Knowledge Transfer: Tests embed the most important knowledge about the system within the code itself, making it easier for teams to understand how everything works.

Behavioral tests not only demonstrate how the code works in typical scenarios but also how it behaves under edge cases or unexpected inputs. These are often missed in traditional documentation but are crucial for maintaining a stable system.

Disclaimer: scope matters

Of course, this approach might not fit every situation. Documentation is essential in many cases, particularly when explaining high-level architecture, business logic, or complex system interactions. My argument is more about shifting the balance—especially in high-turnover environments where keeping traditional documentation up to date can be challenging.

For this specific scope, focusing on tests as living documentation can reduce the need for constantly updated documentation while still maintaining clarity and understanding within the codebase. By focusing on user behavior and writing meaningful tests, you can keep your team productive and your codebase easier to navigate, regardless of staff turnover.

Conclusion

In environments with high turnover, clean code and comprehensive tests provide more value than extensive documentation. Tests force developers to engage with the code and stay in sync with it, while documentation can quickly become outdated.

By focusing on writing behavior-driven tests and maintaining self-explanatory code, you can create a more sustainable system that is easier to maintain and understand, even as team members come and go.

Documentation still has its place, especially for high-level architecture and business logic. But, in a fast-moving environment, tests as living documentation is the more sustainable solution.

Thanks for reading ❤️

Written by Manu

I am a product-driven JavaScript developer, passionate about sharing experiences in the IT world, from a human-centric perspective.

Other articles