annuaire tech France

What's After Unit Testing?

You write unit tests. They run in seconds, they catch regressions and you feel a little smug about it. So what now? Unit tests answer one question well: does this piece work in isolation? Software fails in many other ways, and each needs its own kind of check. Here is a map of what comes next.

What unit tests cannot tell you

A suite of passing unit tests says that each part does what its author expected, given the inputs the author thought of. It says little about whether the parts fit together, whether the author thought of the right inputs, or whether the tests themselves would notice a bug. Each of the techniques below attacks one of those gaps.

Integration tests: do the parts fit?

Unit tests often replace the database, the file system and the network. That makes them fast and also makes them blind to real-world friction: a mismatched column name, a serialization setting, a missing configuration entry.

Integration tests run several real components together. Keep them few and purposeful:

  • Test at the boundaries: repository against a real (test) database, HTTP layer against a hosted instance of the app.
  • Make each test set up and clean up its own data so order does not matter.
  • Accept that they are slower and run them separately from the fast suite.

The balance people talk about is many unit tests, fewer integration tests, and a thin layer of end-to-end tests on top. The exact ratio depends on your system. What matters is that each level covers what the one below cannot.

Property-based testing: let the machine invent inputs

An example-based test says "for this input, expect that output". A property says "for any input, this is always true". The test framework generates many random inputs and checks the property, then shrinks failing cases to a small example.

// Pseudocode of a property
for any list xs:
    Reverse(Reverse(xs)) == xs
    Sort(xs) has the same length as xs
    Sort(xs) is in non-decreasing order

Thinking in properties is useful even without a tool: what must always hold, whatever you feed in? Round trips (serialize then deserialize), idempotence (doing it twice equals doing it once) and invariants (a balance never goes negative) are rich sources. .NET has libraries for this style, and tools that explore code automatically, covered in the story of Moles, Pex, Fakes and IntelliTest, share the same spirit.

Mutation testing: who tests the tests?

Coverage numbers tell you which lines ran, not whether any assertion would catch a mistake. Mutation testing makes a small deliberate change to your code (flip a > to >=, delete a statement, swap a constant) and runs the tests. If they still pass, the mutant "survived", which means your tests would not notice that bug in real life.

It is computationally heavy, so run it on important modules rather than everything. The surviving mutants are a to-do list of missing assertions. A useful low-tech version: occasionally break your own code on purpose and confirm that a test goes red.

Exploratory testing: a human with curiosity

Automated checks verify what you already know. An exploratory session looks for what you do not. A tester, or a developer wearing a tester's hat, uses the application with a goal and a time limit, notes what is odd, and follows the trail. Good sessions have a charter ("explore the checkout with unusual quantities") and short notes, not a script.

Humans notice that a button is confusing, that two screens disagree, or that an error message blames the user. No unit test will tell you that.

Other checks worth knowing

  • Contract tests: verify that a service and its consumer agree about the messages they exchange.
  • Performance tests: measure, do not guess, and compare against a baseline you trust.
  • Static analysis: let tools flag suspicious code before it runs.
  • Monitoring and logging: production is the final test environment; make failures visible quickly.

How to choose

QuestionTechnique
Does each piece do its job?Unit tests
Do the pieces work together?Integration tests
Did I think of the right inputs?Property-based tests
Would my tests catch a bug?Mutation testing
Is it pleasant and sensible to use?Exploratory testing

A sensible next step

  1. Pick the most important feature of your system.
  2. Write one integration test that exercises it through a real boundary.
  3. Find one property of its core logic and test it with generated input.
  4. Break the code deliberately and see which tests notice.
  5. Spend thirty minutes using it like a stranger would.

None of this replaces unit tests. It surrounds them. Keep the fast core, add layers where the risk is greatest, and be honest about what each layer can and cannot promise. And remember that tools only help if you also keep your design testable, which is the subject of funcy testing.

More .NET guides

Looking for a software partner? Browse the independent directory of French tech companies (web agencies, IT consultancies, software publishers; the directory itself is in French).