When doing unit tests the "proper" way, i.e. stubbing every public call and return preset values or mocks, I feel like I'm not actually testing anything. I'm literally looking at my code and creating examples based on the flow of logic through my public methods. And every time the implementation changes, I have to go and change those tests, again, not really feeling that I'm accomplishing anything useful (be it mid- or long-term). I also do integration tests (including non-happy-paths) and I don't really mind the increased testing times. With those, I feel like I'm actually testing for regressions, because they have caught multiple, while all that unit tests do is show me that the implementation of my public method changed, which I already know.
Unit testing is a vast topic, and I feel like I'm the one not understanding something here. What's the decisive advantage of unit testing vs integration testing (excluding the time overhead)?
Best Answer
This sounds like the method you are testing needs several other class instances (which you have to mock), and calls several methods on its own.
This type of code is indeed difficult to unit-test, for the reasons you outline.
What I have found helpful is to split up such classes into:
Then the classes from 1. are easy to unit-test, because they just accept values and return a result. In more complex cases, these classes may need to perform calls on their own, but they will only call classes from 2. (and not directly call e.g. a database function), and the classes from 2. are easy to mock (because they only expose the parts of the wrapped system that you need).
The classes from 2. and 3. cannot usually be meaningfully unit-tested (because they don't do anything useful on their own, they are just "glue" code). OTOH, these classes tend to be relatively simple (and few), so they should be adequately covered by integration tests.
An example
One class
Say you have a class which retrieves a price from a database, applies some discounts and then updates the database.
If you have this all in one class, you'll need to call DB functions, which are hard to mock. In pseudocode:
All three steps will need DB access, so a lot of (complex) mocking, which is likely to break if the code or the DB structure changes.
Split up
You split into three classes: PriceCalculation, PriceRepository, App.
PriceCalculation only does the actual calculation, and gets provided the values it needs. App ties everything together:
That way:
Finally, it may turn out PriceCalculation must do its own database calls. For example because only PriceCalculation knows which data its needs, so it cannot be fetched in advance by App. Then you can pass it an instance of PriceRepository (or some other repository class), custom-tailored to PriceCalculation's needs. This class will then need to be mocked, but this will be simple, because PriceRepository's interface is simple, e.g.
PriceRepository.getPrice(articleNo, contractType)
. Most importantly, PriceRepository's interface isolates PriceCalculation from the database, so changes to the DB schema or data organisation are unlikely to change its interface, and hence to break the mocks.