Thread overview
Re: Instance-specific unittests
Feb 13, 2012
Jonathan M Davis
Feb 13, 2012
James Miller
Feb 14, 2012
H. S. Teoh
Feb 14, 2012
H. S. Teoh
February 13, 2012
On Monday, February 13, 2012 15:12:15 H. S. Teoh 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:
[snip]

Terminology correction: It's once per _instantiation_, not per instance. An instance would be a constructed object.

And yes, this can be useful. The problem that you get into is when you _don't_ want your test to be in each instantiation of the template. In that case, you either end up having to use static if or move those unit tests out of the template.

Regardless, the fact that unittest blocks get compiled into each individual template instantiation is something that anyone writing templated types should know.

- Jonathan M Davis
February 13, 2012
On 14 February 2012 12:26, Jonathan M Davis <jmdavisProg@gmx.com> wrote:
> On Monday, February 13, 2012 15:12:15 H. S. Teoh 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:
> [snip]
>
> Terminology correction: It's once per _instantiation_, not per instance. An instance would be a constructed object.
>
> And yes, this can be useful. The problem that you get into is when you _don't_ want your test to be in each instantiation of the template. In that case, you either end up having to use static if or move those unit tests out of the template.
>
> Regardless, the fact that unittest blocks get compiled into each individual template instantiation is something that anyone writing templated types should know.
>
> - Jonathan M Davis

Its pretty cool, I didn't think about it, but it makes sense, since the compiler essentially makes a new version of the code for each instantiation.

Also I imagine that common-to-all tests should probably be done inside the templates (to ensure that all instantiations have the same behaviour), then more general tests go outside the template, no reason why you can't have two test blocks, especially if you lay the code out so they are close together.
February 14, 2012
On Tue, Feb 14, 2012 at 12:40:13PM +1300, James Miller wrote: [...]
> Its pretty cool, I didn't think about it, but it makes sense, since the compiler essentially makes a new version of the code for each instantiation.
> 
> Also I imagine that common-to-all tests should probably be done inside the templates (to ensure that all instantiations have the same behaviour), then more general tests go outside the template, no reason why you can't have two test blocks, especially if you lay the code out so they are close together.

I usually like to put unittests close to the code they're testing, which means right after a function for tests related to that function. Much of my code consists of alternating function definition / unittest blocks.

Which is what led me to discover this neat feature: unittests don't get access to non-static variables, and you have to create actual instances of structs/classes before you can actually test them, so it's not obvious that they will be run multiple times for each instantiation (thanks, Jonathan!). So I was quite surprised when a failed test case led me to insert some writeln()'s, which produced unexpectedly duplicated output because it ran twice.

In retrospect, it makes a lot of sense, especially given that you can refer to the struct/class without specifying the compile-time parameters, so unittests can apply across all instantiations just by writing something like this:

	struct S(A,B,C,...) {
		...
		unittest {
			S s;
			/* tests common to all S(...)'s. */
		}
	}

Whereas if you placed the unittest outside the struct{}, you'd have to individually test each combination of compile-time parameters of S: a pain for a small number of combinations, totally infeasible as the number of combinations increase (imagine having to test all possible instantiations of struct S(int,int)).

Having the compiler generate multiple versions of the unittest per instantiation is a totally awesome way of making such a situation tractable: only test those instantiations that the program actually uses.


T

-- 
Answer: Because it breaks the logical sequence of discussion. / Question: Why is top posting bad?
February 14, 2012
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
February 17, 2012
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