I agree with the sentiment that when testing, we should use the public API to ensure our tests are not brittle. ie. Don't test the implementation, test the behaviour.
Imagine a situation where my public API will have two methods or features. One is a command method that updates internal private state. The other is a query method, that retrieves info from this private state. I don't want to expose the private fields just for testing.
The challenge is how do I start writing my tests when confirming the behaviour of one requires the other, while following a TDD approach.
Let's imagine a scenario as such:
class Manager:
def Manager():
_reserved_resources = []
def reserve_resource(id):
# Creates a new resource and reserves if none exist in the pool, or reserves an existing one. Does not return anything.
def retrieve_reserved_resource(id):
# Returns a reserved resource, or returns None if not previously reserved.
If I'm using the public API, I can't test the behaviour of reserve_resource until retrieve_reserved_resource is implemented. And I can't test that retrieve_reserved_resource has the correct behaviour, until reserve_resource is implemented.
If my first test is:
def test_reserve_resource_when_no_resources_exists_creates_a_new_resource()
How do I confirm this behaviour when retrieve_reserved_resource is not yet implemented.
Do I just write the test as if retrieve_reserved_resource was implemented, and as such, by the time the test is green, I would have basic implementations of both methods?
This is very specific to TDD. Also understand this is a contrived example, but hoping it clarifies the question. Basically it's a chicken and egg scenario, where the command method and query method both need each other to confirm their behaviour. So where to start?
My heuristic is "reads before writes" -- you can't "drive" any design, or for that matter any implementation, if you can't measure the behavior of the code under test.
So the first tests are all going to be variations of "what do I measure in the simplesst possible case"?
m = Manager()
assert m.retrieve_reserved_resource(12345) is None
Then, when we are satisfied with those tests, we move on to measuring what happens in a slightly more complicated case.
m = Manager()
m.reserve_resource(12345)
assert m.retrieve_reserved_resource(12345) is not None