Tips:

Avoid writing procedural code

Tests can be a bear to maintain if they're written against procedural-style code that relies heavily on global state or lies deep in the body of an ugly method. If you're writing code in an OO language, use OO constructs effectively to reduce this.

  • Avoid global state if at all possible.
  • Avoid statics as they tend to ripple through your codebase and eventually cause things to be static that shouldn't be. They also bloat your test context (see below).
  • Exploit polymorphism effectively to prevent excessive ifs and flags

Find what changes, encapsulate it and separate it from what stays the same.

There are choke points in code that change a lot more frequently than other pieces. Do this in your codebase and your tests will become more healthy.

  • Good encapsulation leads to good, loosely coupled designs.
  • Refactor and modularize.
  • Keep tests small and focused.

The larger the context surrounding a test, the more difficult it will be to maintain.

Do whatever you can to shrink tests and the surrounding context in which they are executed.

  • Use composed method refactoring to test smaller chunks of code.
  • Are you using a newer testing framework like TestNG or JUnit4? They allow you to remove duplication in tests by providing you with more fine-grained hooks into the test lifecycle.
  • Investigate using test doubles (mocks, fakes, stubs) to reduce the size of the test context.
  • Investigate the Test Data Builder pattern.

Remove duplication from tests, but make sure they retain focus.

You probably won't be able to remove all duplication, but still try to remove it where it's causing pain. Make sure you don't remove so much duplication that someone can't come in and tell what the test does at a glance. (See Paul Wheaton's "Evil Unit Tests" article for an alternative explanation of the same concept.)

  • No one will want to fix a test if they can't figure out what it's doing.
  • Follow the Arrange, Act, Assert Pattern.
  • Use only one assertion per test.

Test at the right level to what you're trying to verify.

Think about the complexity involved in a record-and-playback Selenium test and what could change under you versus testing a single method.

  • Isolate dependencies from one another.
  • Use dependency injection/inversion of control.
  • Use test doubles to initialize an object for testing, and make sure you're testing single units of code in isolation.
  • Make sure you're writing relevant tests
    • "Spring the Trap" by introducing a bug on purpose and make sure it gets caught by a test.
  • See also: Integration Tests Are A Scam

Know when to use State Based vs Interaction Based Testing

True unit tests need true isolation. Unit tests don't hit a database or open sockets. Stop at mocking these interactions. Verify you talk to your collaborators correctly, not that the proper result from this method call was "42".

Demonstrate Test-Driving Code

It's up for debate whether or not a given team will take to test-driving all code, or writing "tests first" for every line of code. But should they write at least some tests first? Absolutely. There are scenarios in which test-first is undoubtedly the best way to approach a problem.

  • Try this exercise: TDD as if you meant it (Another Description)
  • See also: Test Driven Development and the Scientific Method

Resources:

  • Test Driven by Lasse Koskela
  • Growing OO Software, Guided by Tests by Steve Freeman and Nat Pryce
  • Working Effectively with Legacy Code by Michael Feathers
  • Specification By Example by Gojko Adzic
  • Blogs to check out: Jay Fields, Andy Glover, Nat Pryce
  • As mentioned in other answers already:
    • XUnit Patterns
    • Test Smells
    • Google Testing Blog
    • "OO Design for Testability" by Miskov Hevery
  • "Evil Unit Tests" by Paul Wheaton
  • "Integration Tests Are A Scam" by J.B. Rainsberger
  • "The Economics of Software Design" by J.B. Rainsberger
  • "Test Driven Development and the Scientific Method" by Rick Mugridge
  • "TDD as if you Meant it" exercise originally by Keith Braithwaite, also workshopped by Gojko Adzic

Are you testing small enough units of code? You shouldn't see too many changes unless you are fundamentally changing everything in your core code.

Once things are stable, you will appreciate the unit tests more, but even now your tests are highlighting the extent to which changes to your framework are propogated through.

It is worth it, stick with it as best you can.


Without more information it's hard to make a decent stab at why you're suffering these problems. Sometimes it's inevitable that changing interfaces etc. will break a lot of things, other times it's down to design problems.

It's a good idea to try and categorise the failures you're seeing. What sort of problems are you having? E.g. is it test maintenance (as in making them compile after refactoring!) due to API changes, or is it down to the behaviour of the API changing? If you can see a pattern, then you can try to change the design of the production code, or better insulate the tests from changing.

If changing a handful of things causes untold devastation to your test suite in many places, there are a few things you can do (most of these are just common unit testing tips):

  • Develop small units of code and test small units of code. Extract interfaces or base classes where it makes sense so that units of code have 'seams' in them. The more dependencies you have to pull in (or worse, instantiate inside the class using 'new'), the more exposed to change your code will be. If each unit of code has a handful of dependencies (sometimes a couple or none at all) then it is better insulated from change.

  • Only ever assert on what the test needs. Don't assert on intermediate, incidental or unrelated state. Design by contract and test by contract (e.g. if you're testing a stack pop method, don't test the count property after pushing -- that should be in a separate test).

    I see this problem quite a bit, especially if each test is a variant. If any of that incidental state changes, it breaks everything that asserts on it (whether the asserts are needed or not).

  • Just as with normal code, use factories and builders in your unit tests. I learned that one when about 40 tests needed a constructor call updated after an API change...

  • Just as importantly, use the front door first. Your tests should always use normal state if it's available. Only used interaction based testing when you have to (i.e. no state to verify against).

Anyway the gist of this is that I'd try to find out why/where the tests are breaking and go from there. Do your best to insulate yourself from change.


One of the benefits of unit testing is that when you make changes like this you can prove that you're not breaking your code. You do have to keep your tests in sync with your framework, but this rather mundane work is a lot easier than trying to figure out what broke when you refactored.