In writing unit tests, for each object that the unit interacts with, I am taking these steps (stolen from my understanding of JBrains\' Integration Tests are a Scam):
First, it's definitely harder to get this level of coverage with integration tests, so I think unit tests are still superior. However, I think you have a point. It's hard to keep your objects' behavior in sync.
An answer to this is to have partial integration tests that have real services 1 level deep, but beyond that are mocks. For instance:
var sut = new SubjectUnderTest(new Service1(Mock.Of(), ...), ...);
This solves the problem of keeping behaviors in sync, but compounds the level of complexity because you now have to setup many more mocks.
You can solve this problem in a functional programming language using discriminated unions. For instance:
// discriminated union
type ResponseType
| Success
| Fail of string // takes an argument of type string
// a function
let saveObject x =
if x = "" then
Fail "argument was empty"
else
// do something
Success
let result = saveObject arg
// handle response types
match result with
| Success -> printf "success"
| Fail msg -> printf "Failure: %s" msg
You define a discriminated union called ResponseType
that has a number of possible states, some of which can take arguments and other metadata. Every time you access a return value you have to deal with possible various conditions. If you were to add another failure type or success type, the compiler would give you warnings for each time you don't handle the new member.
This concept can go a long way toward handling the evolution of a program. C#, Java, Ruby and other languages use exceptions to communicate failure conditions. But these failure conditions are frequently not "exceptional" circumstances at all, which ends up leading to the situation you are dealing with.
I think functional languages are the closest to providing the best answer to your question. Frankly, I don't think there is a perfect answer, or even a good answer in many languages. But compile-time checking can go a long way