Codementor Events

Testability (Part 2)

Published Jul 30, 2018

Repost from thirdengine.blogspot.com

In my previous post (see Testability Part 1), I talked about automated testing in PHP and some of the roadblocks to getting complete test coverage. The three code points that I have found difficult to test most often are native functions, static methods, and the new operator. I covered native functions in part 1, so now let's talk about static methods.

There is an existing article called Static Methods Are Death to Testability. If you have not found that to be true, you either aren't doing a lot of unit testing or you are already not using very many static methods. I won't go into why, that's why I bothered posting a link, but I can help you beat the problem.

So you decide not to use static methods because they aren't easily testable. Now what? A common solution is to just define static methods as regular methods in the same class you normally would. So instead of

1:  class MyClass  
2:  {  
3:    public function printHello()  
4:    {  
5:      print self::print('Hello!');  
6:    }  
7:    
8:    public static function print($string)  
9:    {  
10:      print $string;  
11:    }  
12:  }  

you get

1:  class MyClass  
2:  {  
3:    public function printHello()  
4:    {  
5:      print $this->print('Hello!');  
6:    }  
7:    
8:    public function print($string)  
9:    {  
10:      print $string;  
11:    }  
12:  }  

This is, in fact, much more testable (at least it will be when print() gets more complicated. It seems to have introduced a problem we didn't have before. It's now much harder to determine what the MyClass object is actually capable of, and under what circumstances. What methods require state? What methods modify the object state? To me this is an unnecessary complication of your objects, and there is a simple solution that we use as a convention that is definitely working well for us.

We use the Propel ORM, version 1.6. For generated models Propel has three classes per model: the base object, a query object, and a peer object. The base object represents one record in a "table", and the query object defines how to get records. So what does the peer object do? Mostly, it defines metadata about the object such as how to translate its property names to database column names and information on how to get a query or base object to work with.

If you know anything about how Propel works, you will already know that for each model there are base classes generated with all of the framework code, and then there are also empty, derived classes generated for you to put any of your own code. That way, the framework can update the code in the base classes without fear of damaging code you added. So, we had all of these peer classes sitting empty.

It seemed like a perfect place to put functionality that didn't really have a home anywhere else, namely static methods that would normally live inside the base object classes. If you don't use Propel, don't worry about it. All Propel did for us was jump-start the idea. The basic premise is that you create a second class (which should probably even be a singleton!) that contains your static method for the normal class. See our previous example

1:  class MyClass  
2:  {  
3:    public function printHello()  
4:    {  
5:      $meta = new MyClassMeta();  
6:      print $meta->print('Hello!');  
7:    }  
8:  |  
9:    
10:  class MyClassMeta  
11:  {  
12:    public function print($string)  
13:    {  
14:      print $string;  
15:    }  
16:  }  

Now, you should both be able to test each class easily, they are also discrete, making it clear to consumers which class handles which type of functionality. They will more instinctively know what set of data each method will operate on, making things easier to understand and use. But wait! You might have noticed a problem! The printHello() method is not really that easy to test in isolation yet. For shame ...

That is where the next installment comes in, that will give you the tools to make your entire codebase (or pretty close) testable in a straightforward fashion that makes sure unit testing is a breeze that's easy for your organization to keep up with.

Discover and read more posts from Tony Vance
get started