TDD - Black Box Refactoring?

I have a nontrivial service object designed with TDD. It started with a simple task: for an object from a queue, create an attempt for asynchronous processing. So I wrote a test around my constructAttempt() method:

 void constructAttempt() {...} 

There are many possible scenarios to consider, so I have a dozen tests for this method.


Then I applied what I really needed: scan the entire queue and build a batch of attempts. So the code looks more:

 public void go() { for (QueuedItem item : getQueuedItems()) { constructAttempt(item); } } 

So, I added a new test or two for this go() method.


Finally, I found that I need preprocessing, which can sometimes affect constructAttempt() . Now the code looks more:

 public void go() { preprocess(); for (QueuedItem item : getQueuedItems()) { constructAttempt(item); } } 

I have a few doubts as to what I should do now.

Should I keep the code as is, with constructAttempt() , preprocess() and go() tested independently? Why yes / why not? I risk not covering the side effects of pre-processing and interruption of encapsulation.

Or should I reorganize my entire test suite to only call go() (which is the only public method)? Why yes / why not? This would make the tests a little more obscure, but on the other hand, it would take into account all possible interactions. In fact, this will become a black-field test using only the public API, which may not correspond to TDD.

+6
language-agnostic tdd testing
source share
3 answers

The go method actually just organizes several interactions and is not very interesting in itself. If you write tests against go instead of your sub methods, the tests are likely to be terribly complicated because you have to consider the combinatorial burst of interactions between preprocess and constructAttempt (and maybe even getQueuedItems , although that sounds relatively simple).

Instead, you should write tests for subordinate methods - and tests for constructAttempt should consider all potential effects of the preprocess. If you cannot mimic these side effects (by manipulating the main or dual version), refactor your class until you can.

+6
source share

@Jeff is entitled. In fact, you have two responsibilities arising in this facility. You may want to pull queued items into your class. Click preprocess and constructAttempt on the individual elements. IMHO, if you have a class that deals with individual items, and a list of items, you have a smell of code. Responsibilities are the actions of an item list container.

 public void go() { for (QueuedItem item : getQueuedItems()) { item.preprocess(); item.constructAttempt(); } } 

Note. This is similar to working with a command object template

[EDIT 1a] This makes it very easy to mock test. The go method requires only checking with one queue element or without queue elements. In addition, each item can now have its own individual tests separately from the go combinatorial explosion.

[EDIT 1b] You can even add preprocess to an element:

 public void go() { for (QueuedItem asyncCommunication: getQueuedItems()) { asyncCommunication.attempt(); } } 

You now have a true command pattern .

+3
source share

I am saying that your test suite only calls go (), as it is the only public API. This means that after you have reviewed all the scripts for the go method (which will include the preprocess and the queue), then it doesn’t matter if you change the internal implementation. Your class remains true in terms of public use.

I hope that you use dependency inversion for the classes used for preprocessing / loading the queue, so you can test the preprocessing yourself and then mock it in the go () test.

+1
source share

All Articles