[nycphp-talk] Why do unit tests not inherit?
Gary A. Mort
garyamort at gmail.com
Wed Dec 4 15:26:39 EST 2013
On 11/30/2013 06:26 PM, Robert Stoll wrote:
> I am glad you coming back to me with this, I find it to be a very interesting topic
>
>> -----Original Message-----
>> From: talk-bounces at lists.nyphp.org [mailto:talk-bounces at lists.nyphp.org] On Behalf Of Gary A. Mort
>> Sent: Friday, November 22, 2013 8:46 PM
>> To: NYPHP Talk
>> Subject: Re: [nycphp-talk] Why do unit tests not inherit?
>>
>> Thanks Robert... I may be misunderstanding something here:
>>
>> On 11/15/2013 12:57 PM, Robert Stoll wrote:
>>> I am not sure if we talk about the same. Just to avoid misunderstands I am going to outline a little bit more what I
>>> meant. I did not mean that each method of a class has to have its one test class. But each method of a class A
> should
>>> have an own test method in the test class T. And if the method of class A has branches, let's say one if-statement,
> then
>>> the ideal case would be that you create two test methods in C which covers both cases. Once for the case that the
>>> if-condition evaluates to true and once to false.
>>> For example:
>>>
>>> class A{
>>> private $_isActive=false;
>>> function isActive(){
>>> return $this->_isActive;
>>> }
>>> function foo(){
>>> $this->_isActive=true;
>>> }
>>>
>>> function bar(){
>>> if($isActive){
>>> doesThis();
>>> } else{
>>> doesThat();
>>> }
>>> }
>>> }
>>>
>>> class T extends SomeTestFramework{
>>> public function testFoo_Standard_IsActiveSetToTrue (){
>>> // arrange
>>> // act
>>> // assert
>>> }
>>> public function testBar_IsActiveIsTrue_DoesThis(){}
>> I am assuming that there is no unit testing magic which is setting
>> things, so this method would actually be:
>>
>> public function testBar_IsActiveIsTrue_DoesThis(){
>>
>> // create an object $testObject of class A
>> // call $testObject->foo() to make it active
>> // Test that $testObject->isActive returns true
>> // Test that $testObject->bar executes doesThis()
>>
>> }
>>
>>
>> public function testBar_IsActiveIsFalse_DoesThat(){
>>
>> // create an object $testObject of class A
>> // Test that $testObject->isActive returns false
>> // Test that $testObject->bar executes doesThat()
>>
>> }
> [Robert Stoll]
> That right, that's how my tests would look like more or less with the slight difference that I would not test if
> isActive is true or false (I would cover it in another test case) but it's ok to test that as well.
>
>> It's with the above commented steps that I have an issue. Primarily
>> because in practice, if someone creates:
>>
>> Class APrime extend A{}
>>
>> Then they also create
>>
>> class TPrime extends SomeTestFramework{
>>
>> function bar(){
>> if($isActive){
>> doesThis();
>> } else{
>> doesNOTDOThat();
>> }
>>
>> }
> [Robert Stoll]
> I guess you made a mistake, the new behaviour should be in APrime (as in the next paragraph) and not in TPrime:
> class APrime extend A{
> function bar(){
> if($isActive){
> doesThis();
> } else{
> doesNOTDOThat();
> }
> }
>
>> In in TPrime will be:
>>
>>
>> public function testBar_IsActiveIsTrue_DoesThis(){
>>
>> // create an object $testObject of class APrime
>> // call $testObject->foo() to make it active
>> // Test that $testObject->isActive returns true
>> // Test that $testObject->bar executes doesThis()
>>
>> }
>>
>>
>> public function testBar_IsActiveIsFalse_DoesNOTDOThat(){
>>
>> // create an object $testObject of class APrime
>> // Test that $testObject->isActive returns false
>> // Test that $testObject->bar executes doesNOTDOThat()
>>
>> }
>>
>> So everything has been cut and pasted from one to the other. The only
>> difference is that APrime calls doesNOTDOThat instead of doesThat.
>> Testing items where taken from one to the other, with minor editing
>> changes to half of the new tests to change doesThat to doesNOTDOThat
> [Robert Stoll]
> I agree, but first of all the question arises, does APrime not already break the Liskov Substitution Principle by
> invoking a different method (doesNOTDOThat instead of doesThat)?
Mostly the situation I was trying to describe but not get into nitty
details are situations where you have external dependencies.
Take storing data to a cache of some sort as a really good example.
The lifecycle of a cache engine could be:
1) Abstact CacheEngine class where you define an isSupported method
which will always return false since you can't store items in the
abstract class. [Yes, with PHP 5.4+ this would instead be better
defined as an interface, but we can't all refuse to support 5.3. :-)]
2) A child class, CacheEngineMemcache where you can run some check to
see if Memcache works[on my mind mainly because I just had to create a
new class for this in Joomla, CacheEngineGaeMemcache because the Joomla
platform checks to see if the Memcache extension is loaded AND the
Memcache class exists in it's implementation. Google happens to provide
both free and paid usage of Memcache for Google App Engine - but they
don't use the PHP Memcache extension, the code is included in their GAE
extension to interface with their setup.]
3) A third child class, CacheEngineMemcached which since it shares 90%
of the same code as Memcache, subclasses CacheEngineMemcache but
modifies isSupported to check for Memcached instead.
So you have 3 classes each implementing the same method[isSupported]
which will return either true or false depending on some underlying
configuration. Using various PHP extensions it's possible to
dynamically load/unload the extension so you can confirm your tests - as
long as isSupported always returns true and false.
Things get extended, changed, modified beyond belief. Some day for some
reason, someone may decide that for their engine, they may return 3
instead of TRUE for isSupported under some odd situation - maybe to
indicate the version of something being supported. Due to the beautiful
nature of PHP, when doing simple true/false checks 3 will show up as
true, so it is a very easy way to be backwardly compatible and add some
extra function.
One answer is, of course, "well, that was a bad design decision. I
don't see any reason to design unit tests to prevent bad design decisions."
But to me, the entire point of unit tests IS to prevent compatibility
issues, re-introduction of bugs, etc. The whole point of programming in
PHP is that PHP programmers are free to code things any way we want
because PHP doesn't have strict typing and all that other stuff. It's
made to do fast, fun coding and serious coding - so it allows college
kids to create things like Facebook as a fun, riddled with holes
project, and then refine it when it becomes popular - and it allows one
to establish a coding discipline inside a company to maintain that
insanely popular franchise.
From what I see with unit testing in practice[open source projects],
there is an assumption that the fixed coding structure is going to be
followed - and so what actually occurs is that unit tests are only
useful for 2-3 years, 5 years at most. At that point, there will be
some new "cool" design pattern that everyone will be switching
to[because it's fun...not because it is "good coding practice"] and all
that "old ugly code" will get bent to fit the new practice and the unit
tests don't help much to discover the problems.
Hmm, rambling out loud so I'll end it there.... it's more meta design
than anything else... I think part of my issue is I have a different
viewpoint then most programmers. I don't tend to come into a project
and say "you need to throw out all this old code and do things the new
one true way", I try to integrate things. When you integrate, you see
problems where design patterns shifted and while a unit test could have
detected it - they weren't written to do so.
Whereas if you can restrict a project to a common framework where
everything works the same way, and the way you want - then it doesn't
make sense to have tests to check to see if things are still following
that pattern - everyone follows that pattern or they aren't on the team.
Both are valid approaches...the latter is a lot easier to extend and
maintain - but it's a lot more expensive to implement[especially with
company mergers where you have to throw away all the IT infrastructure
of one company to be on a common platform].
Whereas integration can lead to a maintenance nightmare in the long run
- but it keeps the budget down in the short run and avoids spending
large sums of money on exploratory projects which may get cancelled.
More information about the talk
mailing list