Verdict
No
Do not write tests for an interface that only declares methods; test each class that implements it, with one shared contract test when several classes implement the same interface.
Why
- Blast radiususers
- Change frequencyrarely
- Detectabilityimmediately
- Reversibilitytrivial
- Test costtrivial
Do not write tests for an interface that only declares methods; test the classes that implement it. For the typical case, a UserStore interface in Java, C# or TypeScript with one or two implementations, Blast radius is users because the implementations serve user features, and Change frequency is rarely because the interface changes a few times a year. Detectability is immediately: the compiler rejects a class that misses or mistypes a declared method. Reversibility is trivial because a revert undoes a wrong declaration, and Test cost is trivial, so rule R8 gives Do not test.
| When | Decision | Why |
|---|---|---|
| An interface method has a default body with logic, such as formatting a display name | Test the default method minimally, through a small implementing class in the test | Detectability moves to same-day: a wrong default body breaks every implementation at once |
| A default method on the interface calculates the tax on an order total | Test mandatory, including rounding and boundary amounts | Blast radius rises to money, Reversibility to costly and Detectability to eventually |
| Tests use an in-memory implementation of a repository interface, and production uses the Postgres one | Test: one contract suite that runs against both implementations | Change frequency rises to regularly, Detectability to eventually, Reversibility to with-effort and Test cost to moderate: the fake drifts from the real store |
| The interface ships in a library, and other teams write the implementations | Test minimally: publish a contract test kit that each implementing team runs | Detectability moves to eventually, Reversibility to with-effort and Test cost to moderate: other teams find the breakage after their release |
| Spring Data generates the implementation of a repository interface from its query method names | Test: one database test per declared query method | Change frequency rises to regularly, Detectability to eventually, Reversibility to with-effort and Test cost to moderate: a query method named with Or in place of And returns plausible wrong rows, and code that updates or emails those rows leaves records to repair |
What breaks if you don't test
An interface without tests of its own breaks nothing that the compiler misses, because a declaration has no body. The failures that reach users sit in the implementations. A PostgresUserStore that compares emails case-sensitively misses a user who signed up as ana@example.com and now types ANA@example.com, so the signup form creates a second account. Support notices weeks later, when she asks where her order history went. A test of PostgresUserStore finds that bug; a test of the UserStore declaration cannot.
What you lose if you over-test
The common over-test uses reflection to assert that UserStore declares save and findByEmail with the right parameter types. The compiler already checks those signatures in every implementation and caller, so the test catches nothing that the build misses, and each signature change needs a second edit in the test. Another over-test mocks the interface and asserts that the mock returns its stubbed value. That test checks the mocking library and none of your code, yet it raises the coverage number.
What to do instead
Test each implementation through the interface type: unit tests for classes that work in memory, integration tests for classes that talk to a database. When several classes implement one interface, write the shared expectations once as a contract test and run it against each class. In JUnit 5 and later, declare the tests as default methods in a test interface, as the JUnit guide to test interfaces shows. In pytest, a parametrised fixture runs every test once per implementation.
When the answer changes
- An interface method gains a default body with logic, such as a formatting rule or a tax calculation.
- A second implementation appears, and the test suite relies on one class while production runs the other.
- A framework generates the implementation from the interface, as Spring Data does for repository query methods.
Code example
One contract for two stores
Every UserStore must pass the contract below. InMemoryUserStore serves the fast service tests, and PostgresUserStore runs in production:
interface UserStoreContract {
UserStore createStore();
@Test
default void findsSavedUserByEmailIgnoringCase() {
UserStore store = createStore();
store.save(new User("ana@example.com"));
assertTrue(store.findByEmail("ANA@example.com").isPresent());
}
@Test
default void returnsEmptyForUnknownEmail() {
assertTrue(createStore().findByEmail("bob@example.com").isEmpty());
}
}
class InMemoryUserStoreTest implements UserStoreContract {
public UserStore createStore() { return new InMemoryUserStore(); }
}
class PostgresUserStoreTest implements UserStoreContract {
public UserStore createStore() { return new PostgresUserStore(TestDatabase.dataSource()); }
}
Each test class supplies only a store. If the Postgres class matches emails case-sensitively, or the in-memory class compares them with equals(), findsSavedUserByEmailIgnoringCase fails for that class in the next CI run. Without the contract, the service tests pass against a fake that disagrees with production.
Related questions
FAQ
- Do you unit test interfaces?
No, an interface that only declares methods has no code that can fail, so it needs no unit test of its own. Unit test each class that implements it, through the interface type.
- Do I need to test each implementation of an interface?
Yes, each implementation holds its own code and needs its own tests. Write the shared expectations once as a contract test, run it against every implementation, and add tests for what each class does beyond the contract.
- Should I test default methods in Java interfaces?
Yes, a Java default method has a body, so test it with one or two tests of the main path. Declare a minimal class in the test that implements the abstract methods with fixed values, then call the default method and assert on the result.
- Should I test Spring Data repository interfaces?
Yes, test each query method that you declare with one test against a database, because Spring Data generates its query from the method name. A method named
findByStatusOrEmailwherefindByStatusAndEmailwas meant starts without errors and returns plausible wrong rows, and code that updates or emails those rows leaves records to repair. Inherited methods such assaveandfindByIdneed no tests of yours.