Adversarial vs Aspirational
I think this is the problem you've tripped over.
In TDD the process is this:
- Write an Aspirational test describing the behaviour desired.
- Write the code in the unit that makes this test pass, while keeping the other tests passing.
- Review the Aspirations and make sure they are what you aspire too.
- Update your Aspirations
When your boss/manager/team lead is turning around and saying that it should be tested (in the context of TDD) they are talking about these Aspirational tests. In this methodology it makes sense to write a test that describes happy and unhappy paths even for the most trivial of cases - because these aren't tests. They are a design document. Perhaps it is better to think of them as self-checking properties.
However I suspect you are instead hearing that you should write the other kind of test - the adversarial kind.
In this kind of test you look at general knowledge (check box testing), the spec (black box testing) or the actual implementation (white box testing) and you hunt for actual weak points.
A check box test for example would fuzz an input field. Not because you know anything specific about it, but because it is a general attack that might happen, and you want to make sure the system can handle it.
A black box test might for example see what happens when you
empty() stack. Does it behave right? What if it was quadruple
A white box test will look for that one statement that dereferences a
null, or sets up an infinite loop, and proves that there is a problem here.
In which case, testing that
if is just a non-starter. It doesn't make sense, an
if(bool) is guaranteed to work. If it doesn't there is an issue with the platform/compiler not the code itself. (Which might be useful to know, but isn't the point either).
It is always easier to write Aspirational tests up-front. Otherwise it feels like you are rehashing the implementation, and it feels like you are just Yes Manning the whole thing.
If you are put in such a position, try to ignore the implementation. Read the user stories, or look at the users of the function (not the function itself). Cobble together the expectations, this paints the acceptability picture from both a business (from story) and usage (from code that calls it) perspective. This helps to keep the tests away from prescribing an implementation. It also helps with the Yes Man feeling.
Adversarial tests on the other hand can only be written against an implementation. By which I mean, that you only know how to attack it once something is known about it.
- Once it is decided that the input is a textbox, then you can write ways to attack it.
- Once you know the semantics of the interface, you can write ways to attack it.
- Once you have an implementation to examine, you can write ways to attack it.
If you are tasked with writing them be aware of the depth of the attack you can provide at this point in the development process. Also be aware of your own attachment to the work, if you are overly attached it will lead to tests that coddle the code instead of revealing its weaknesses.
- If it's your own code - If possible leave time between when you implement it, and when you attack it (several months is a good start).
- leverage a check list of general issues, tailor it with knowledge about the dev who wrote it (if that's yourself honestly record and generalise the issues you tend to make).
- get someone else to have a go at the code. (and add those issues to the list).
- look at the tests already written and ask if the test would reveal something by going one step further.
- look at logs of known issues in the system, they are the breadcrumb trails of successful adversaries.