TDD - rough notes and guidelines to making code unit testable

some really short + rough guidelines for making code easier to use with TDD (Test Driven Development).

TDD is a difficult practice at times, especially when working under pressure.

So I am collecting some guidelines in this post, for quick reference.

Plan to add and review these tips over time, as I learn more ...



Tip 0. use configuration objects to pass around details required to construct objects.
By a configuration object, I mean a simple class with all of the values required to construct an object.
Example: (in C#, but the pattern applies to any language):



 //create the configuration object. The constructor should require ALL the values that are needed to have a correct configuration. 

 LockAcuirerConfig config = new LockAcuirerConfig( 

 _ExpireAfterDuration: new TimeSpan(0,0,1,0), 

 _HostName: "host1" 

 ); 

 //create the target object, passing in the single config object: 

 LockAcquirer acquirer = new LockAcquirer(config);  



Using such a config object means:
a) it is easy to later add more construction values, without having to changes lots of intermediate code.  This is because we simply pass around the config object, rather than ALL of the different values required to create our target object.
b) it is easier to ensure objects are correctly configured (since the object cannot be created, without passing in a Config object).  If the created object LockAcquirer is later changed to need more construction values, then the we can use the compiler to enforce all code that creates LockAcquirerConfig to be updated.
c) it is easier to unit test the constructed object, as it is configured by a Config object and not by a config file or database or any other external source.  This of course assumes that the Config object is not referencing  any such  external resources.

Note: sometimes you will need your target object to have a default (no parameters) constructor.
Example: if you want to expose your object to COM clients, or if you are making a POCO class for Entity Framework.  In this case, make a single Configure(config) method, that fully configures the target object.
Example:



 //create the configuration object. The constructor should require ALL the values that are needed to have a correct configuration. 

 LockAcuirerConfig config = new LockAcuirerConfig( 

 _ExpireAfterDuration: new TimeSpan(0,0,1,0), 

 _HostName: "host1" 

 ); 

 //create the target object, with no parameters: 

 LockAcquirer acquirer = new LockAcquirer(); 

 //pass the config object, to fully configure the target object: 

 acquirer.Configure(config);  



Tip 1. when testing a library (.dll) it's best *not* to have a config file for the library.
The reason is, config files are quite awkard to use with unit tests, which makes it harder to deploy the tests to different machines.  This makes the code harder to automatically test.

Instead: have a top-level 'manager' class in the library, that can only be constructed with a custom Config class.   That way, the unit tests can configure the library, without involving config files.
It also makes the library much easier to use and configure, for the client programmer.

Overhead: each client program will need to have its own config file, with the full set of config settings that the library requires.  But in my experience, this is less of an overhead than working with unit tests + config files.
It also ensures that client MUST fully configure the library.

Tip 2. do NOT use properties to configure a class.
Using 'set' properties to configure a class, is quite fragile, since a client programmer can easily forget to set a property.  Also, if you later add in a new property, it is easy to forget to change all of the client code to also set this new property.

A workaround I have seen for this problem, is that the target object at some point performs a self-validation, to see that all required values have been provided.  However it is better to use the compiler to do this, as the client programmer gets instead feedback from the compiler, rather than relying on runtime checks.

Best follow tip #1 - in this case, the compiler will find all of the configuration points for you (since when you add a new argument to the Config classes constructor, the code will refuse to compile, until you have updated all of the client code).

Tip 3. put as much functionality as possible into libraries (DLLs).
Reasoning:
a) they are easier to re-use
b) they are easier to unit test

Tip 4. *Always* mock out external resources
This may go without saying, but it is so important, that it cannot repeated often enough.
Any 'awkward' external dependency should be facaded out with an interface, to provide an IOC (Inversion Of Control) point.  This allows us to mock out such external resources, which means unit tests are:
- deployable
- repeatable
- focussed on testing *your* code, not the external resource

External resources defined:
- database
- library or software that requires hardware
- any 'difficult' (often, but not always, thirdparty) system you are integrating with
- config files

Some people would include 'data files' in the definition of External Resources.
I would argue that writing unit tests that depend on data files is fine, as long as:
- the files are in source control
- the unit test can be run quickly (in about 1 second or less)


Tip 5. try to avoid static data

Static members have a habit of hanging around in-between tests, which then means you need to write and maintain clean up code, to clean up the static data after each test has run.

So in general it is best to avoid having any static data members.

In some cases - this is difficult, for example if (legitimately) using the Singleton design pattern.
The Singleton pattern is appropriate if an object *truly* is a Singleton - for example, a Card can be a Singleton in an ATM software system, if there really only ever is 1 card object.

Not sure what the best solution is for this - perhaps having a SingletonManager, that knows about all the Singletons in a system, and can be used by unit test code, to reset all of the singletons at once.


Comments