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.
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:
- Classes with the actual "business logic". These use few or no calls to other classes and are easy to test (value(s) in - value out).
- Classes that interface with external systems (files, database, etc.). These wrap the external system and provide a convenient interface for your needs.
- Classes that "tie everything together"
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:
1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database
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:
App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices
That way:
- PriceCalculation encapsulates the "business logic". It's easy to test because it does not call anything on its own.
- PriceRepository can be pseudo-unit-tested by setting up a mock database and testing the read and update calls. It has little logic, hence few codepaths, so you do not need too many of these tests.
- App cannot be meaningfully unit-tested, because it is glue-code. However, it too is very simple, so integration testing should be enough. If later App gets too complex, you break out more "business-logic" classes.
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.
Best Answer
I think it's natural to encounter a divide within unit testing. There are many different opinions on how to do it properly and naturally all other opinions are inherently wrong. There are quite a few articles on DrDobbs recently that explore this very issue to which I link at the end of my answer.
The first problem I see with tests is that it's easy to get them wrong. In my college C++ class we were exposed to unit tests in both the first and second semester. We knew nothing about programming in general in either of the semesters- we were trying to learn the fundamentals of programming via C++. Now imagine telling the students, "Oh hey, you wrote a little yearly tax calculator! Now write some unit tests to ensure that it works correctly." The results should be obvious- they were all horrible, including my attempts.
Once you admit that you suck at writing unit tests and wish to get better, you will soon be faced with either trendy styles of testing or different methodologies. By testing methodologies I refer to practices such as test-first or what Andrew Binstock of DrDobbs does, which is write the tests alongside the code. Both have their pros and cons and I refuse to go into any subjective detail because that will incite a flame war. If you're not confused about which programming methodology is better, then maybe the style of testing will do the trick. Should you use TDD, BDD, Property-based testing? JUnit has advanced concepts called Theories that blurs the line between TDD and Property-based testing. Which to use when?
tl;dr It's easy to get testing wrong, it's incredibly opinionated and I don't believe that any one testing methodology is inherently better as long as they are used diligently and professionally within the context that they're appropriate in. Furthermore, testing is in my mind an extension to assertions or sanity-tests that used to ensure a fail-fast ad-hoc approach to development which is now much, much easier.
For a subjective opinion, I prefer to write "phases" of tests, for lack of a better phrase. I write unit tests which test classes in isolation, using mocks where necessary. These will probably be executed with JUnit or something similar. Then I write integration or acceptance tests, these are run separately and usually only a few times a day. These are your non-trivial use case. I usually use BDD as it is nice to express features in natural language, something that JUnit cannot easily provide.
Lastly, resources. These will present conflicting opinions mostly centered around unit testing in different languages and with different frameworks. They should present the divide in ideology and methodology while allowing you to make up your own opinion as long as I haven't manipulated yours too much already :)
[1] The Corruption of Agile by Andrew Binstock
[2] Response to the Responses of the previous article
[3] Response to Corruption of Agile by Uncle Bob
[4]Response to Corruption of Agile by Rob Myers
[5]Why Bother With Cucumber Testing?
[6] You're Cuking it Wrong
[7]Step Away From the Tools
[8]Commentary on 'Roman Numerals Kata with Commentary'
[9]Roman Numerals Kata With Commentary