www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Instance-specific unittests

reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
I discovered something really cool today, and I thought I'd share it
with my fellow learners:

The unittest block is used for inserting unit tests that are executed at
runtime before main() is called. They are very useful for inserting
little tests after a piece of complex code, to make sure it actually
works as expected.

What's cool is that if you have a unittest block inside a class or
struct with compile-time parameters:

	struct S(bool B) {
		void method() { ... }
		unittest {
			/* test method() for correct operation */
		}
	}

then the unittest will be executed once *per instance* of S.  That is,
if your program uses both S!true and S!false, the unittest will run
twice, once for each instance. This ensures that the unittest tests all
variants of the code introduced by the compile-time parameter, up to
what your program actually uses.

But what if your unittest wants to test for a specific behaviour in a
specific instance of S?  You could write, for example:

	struct S(bool B) {
		void method() {
			static if (B) {
				/* Behaviour X */
			} else {
				/* Behaviour Y */
			}
		}
		unittest {
			S!true s;
			assert(/* test for behaviour X */);

			S!false t;
			assert(/* test for behaviour Y */);
		}
	}

The problem is that now the unittest will still run twice, but each time
it does exactly the same thing. Here's where another static if comes in
to rescue:

	struct S(bool B) {
		void method() {
			static if (B) {
				/* Behaviour X */
			} else {
				/* Behaviour Y */
			}
		}
		unittest {
			static if (B) {
				S!true s;
				assert(/* test for behaviour X */);
			} else {
				S!false t;
				assert(/* test for behaviour Y */);
			}
		}
	}

But we can do even better: since inside the static if, the value of B is
already known, we take advantage of the fact that we're inside the
parametrized scope of S, and so we can refer to the current instance of
S just by referring to "S":

	struct S(bool B) {
		void method() {
			static if (B) {
				/* Behaviour X */
			} else {
				/* Behaviour Y */
			}
		}
		unittest {
			static if (B) {
				S s;	// here S == S!true
				assert(/* test for behaviour X */);
			} else {
				S t;	// here S == S!false
				assert(/* test for behaviour Y */);
			}

			/* Test here for behaviour common to both
			 * variants */
		}
	}

And here you have it: a very clean and concise way to unittest different
compile-time variants of a struct/class. 


T

-- 
Ruby is essentially Perl minus Wall.
Feb 13 2012
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Mon, 13 Feb 2012 18:12:15 -0500, H. S. Teoh <hsteoh quickfur.ath.cx>  
wrote:

 I discovered something really cool today, and I thought I'd share it
 with my fellow learners:

 The unittest block is used for inserting unit tests that are executed at
 runtime before main() is called. They are very useful for inserting
 little tests after a piece of complex code, to make sure it actually
 works as expected.

 What's cool is that if you have a unittest block inside a class or
 struct with compile-time parameters:

 	struct S(bool B) {
 		void method() { ... }
 		unittest {
 			/* test method() for correct operation */
 		}
 	}

 then the unittest will be executed once *per instance* of S.

[snip] As Jonathan suggests, it's once per instantiation, though I think that's what you meant. Dcollections has been doing this ever since the D2 branch was introduced (circa May 2010). I use the following idiom to great success: class SomeClass(T) { enum doUnitTests = isIntegral!T; static if(doUnitTests) unittest { ... // unit test using the assumption that T is integral type. } } // at bottom of file: unittest { SomeClass!int intv; // run int unit tests SomeClass!uint uintv; // run uint unit tests ... } One other really nice benefit is that you don't have to specify template parameters, just use the class identifier inside the unit test. I have found the following issues with doing stuff this way: 1. The more values of T you try to test for, the more difficult it becomes to make a "generalized" unit test. But for the most part, I've found that for integral types, you can do a lot with integer literals. Making helper functions that construct your object using the same literal parameters helps immensely. 2. It's not enough to just rely on your global unit test to instantiate just the right ones -- because a template can be instantiated by a user of your lib while in unit test mode, you never know in what mode your template will be instantiated. 3. If you are making classes like this, make *sure* all your unit test helper functions are non-virtual! Otherwise, if some code instantiates with unit tests on and some off, you will have vtable inconsistencies. I'd link to example source on dcollections, but dsource appears to be down (github here I come!) -Steve
Feb 13 2012
parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Mon, Feb 13, 2012 at 07:15:18PM -0500, Steven Schveighoffer wrote:
[...]
 3. If you are making classes like this, make *sure* all your unit
 test helper functions are non-virtual!  Otherwise, if some code
 instantiates with unit tests on and some off, you will have vtable
 inconsistencies.

[...] I usually just use version(unittest){...} at the top of the file to make module-wide unittest helper functions. I don't like modifying the thing I'm testing just by the act of testing it (e.g., adding class members when running unittests for that class). This may be a fact of life in quantum physics, but for testing code I prefer the "independent observer" mode of operation. :) T -- Lottery: tax on the stupid. -- Slashdotter
Feb 13 2012
parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Mon, 13 Feb 2012 19:32:31 -0500, H. S. Teoh <hsteoh quickfur.ath.cx>  
wrote:

 On Mon, Feb 13, 2012 at 07:15:18PM -0500, Steven Schveighoffer wrote:
 [...]
 3. If you are making classes like this, make *sure* all your unit
 test helper functions are non-virtual!  Otherwise, if some code
 instantiates with unit tests on and some off, you will have vtable
 inconsistencies.

[...] I usually just use version(unittest){...} at the top of the file to make module-wide unittest helper functions. I don't like modifying the thing I'm testing just by the act of testing it (e.g., adding class members when running unittests for that class). This may be a fact of life in quantum physics, but for testing code I prefer the "independent observer" mode of operation. :)

I do that in some cases too. But if one of your arguments is the object in question, I feel it is cleaner because: 1. You are not potentially allowing free-instantiation of a template for all types 2. You have much better control over visibility/protection on a class member than a module member. Technically, adding a final method does not change the object in question. It's just a syntax convenience. However, I like the fact that if the object is a template, I can control when the unit test instantiates, and whether or not the helper function is defined. -Steve
Feb 17 2012