ClassTester, or "How to exercise your assembly"
Fear not, brethren and...uhhh....sistren. The resistance is still alive. But the revolutionary jargon will get in the way here so I'm reverting to a more traditional means of expression.
Among the things we've implemented as part of our project clean-up is the aptly-named ClassTester. It's a neat little tool that takes some of the pain out of routine testing of constructors and properties by, for example, setting a property then checking to make sure you get the same value back from the getter. Here's some sample syntax for testing the properties of an object:
ClassTester tester = new ClassTester(new myObject());
tester.TestProperties();
Now, hillbillies are not what you might call amenable to writing boilerplate code for every single class in their domain. Leads to some problems you can only imagine in your nightmares. So in an attempt to avoid duplication, I decided instead to walk through all the classes in the assembly:
[Test] 1 public void Test_general_constructors_and_properties() 2 { 3 Assembly assembly = Assembly.GetAssembly(typeof (Still)); 4 Type[] types = assembly.GetTypes(); 5 foreach (Type type in types) 6 { 7 if ( type.IsClass 8 && !type.IsAbstract 9 && type.Namespace == "Suvius.Applications.HoochDeriver.Domain" )) 10 { 11 ClassTester.TestConstructors(type, false); 12 object item = assembly.CreateInstance(type.FullName); 13 ClassTester tester = new ClassTester(item); 14 15 tester.TestProperties(); 16 } 17 } 18 }
Some points of interest:
- I could use any domain object in line 3. I'm interested only in getting the assembly that houses all the domain objects
- Line 8: The ClassTester doesn't much like abstract classes
- Line 9: This line may seem redundant but it is necessary because of the way I run my builds (which, credit where it's due, I pilfered from JP Boodhoo). Namely, in the build file, we compile all .cs files into a single assembly and run the tests against it. Which means line 3, will retrieve the assembly containing (almost) every class in the application. Compare this with compiling in the IDE where all domain classes are in one assembly by themselves. So running this test from the IDE, where it will compile the assemblies separately, we don't need the check for the namespace. Running it in the build file however, we do.
There are in fact two separate tests here, one for constructors and one for properties. If you want to argue that these should be separate tests, I'll agree. If you want to argue against that, I'll agree too. That's how much I care about it.
Anyway, this code is nice in theory. At present, the tester handles only very routine cases. If there are any validation rules, you'll get validation errors. For example, if you have a Percent property where you throw an exception if the value is not 'twixt 0 and 100, the ClassTester will throw this exception most of the time because it generates a random number to set the properties and doesn't know about your little rules. The ClassTester does allow you to ignore individual properties though, which is useful when testing classes individually.
The second problem with looping through every class is that not every class can be tested, even with no validation rules. In our case, we had some classes that were inheriting from CollectionBase (did I mention it was ported over from .NET 1.1?) and the ClassTester failed on all of these because of the Capacity property (which I didn't know existed).
The end result is that we had to include a list of classes to ignore from the domain and make sure they were tested separately through more conventional methods. Whether or not this is worth it depends on how many classes you need to ignore. In our case, it was only the collection classes which have since been removed anyway because of the generic collections in .NET 2.0.
And to prove it was all worthwhile, the ClassTester found and located no less than three bugs in our getters and setters, each one a variation on this theme:
public decimal AmountOfHopsToAdd { get { if (DandelionBase != null) { // Return some complicated formulaic value based on DandelionBase
} else return 0.0M; } set { _amountOfHopsToAdd = value; } }
In this specific case, the setter is all but useless since _amountOfHopsToAdd is never actually used anywhere. The other examples gave similar fodder for pondering and our code is shinier and happier for it.
Kyle the Testable