Discharging Static #2

In the first article in this series, I wrote about the problems with testing static methods in classes, and showed a few approaches that allow us to write mocks for statics. Testing classes where we have static properties is a lot harder to manage, because any changes made in any test to those properties are global, across the entire test suite, not simply the test that we are running. Static properties are part of the global state, and we can’t simply tearDown() those changes in the way that we do with instances — at least we cannot easily do so if the property has a visibility of private or protected.

When we change a static property in one class, we can’t always anticipate the affects elsewhere in the system, or in other classes. Consider the following snippet of code, a simple inheritance:


abstract class baseClass {
    protected static $value = 1;
    
    public static function getValue() {
        return self::$value;
    }

    public static function setValue($value) {
        self::$value = $value;
    }
}

class one extends baseClass {
}

class two extends baseClass {
}

echo one::getValue();

two::setValue(2);

So now class one has a static $value property of 1, and class two has a static $value property of 2 doesn’t it? Well “No!”, because in calling two::setValue(2), we’ve actually changed the $value property in baseClass; we only appear to have $value properties in classes one and two because they extend baseClass, but there is only a single static $value property shared between them, and when we change it through either child class, we’re really only changing it in the parent class. We could just as easily change it using baseClass::setValue(). Changing it by calling the setValue() method through classes one or two is misleading and counter-intuitive to what is actually happening; yet it is still an easy mistake to make.

Even if we define the method calls to use static rather than self, so that we’re using late static binding:


    public static function getValue() {
        return static::$value;
    }

    public static function setValue($value) {
        static::$value = $value;
    }

this still won’t resolve to separate static properties for each child class, because the property itself is still defined in baseClass, and that’s the property we’re referencing: late static binding won’t create new properties in the child classes when they don’t exist. We need to define separate properties in each child class if we want to maintain them independently.


abstract class base {
    protected static $value = 0;
    
    public static function getValue() {
        return static::$value;
    }

    public static function setValue($value) {
        static::$value = $value;
    }
}

class one extends base {
    protected static $value = 1;
}

class two extends base {
    protected static $value = 2;
}

This simple example demonstrates how easy it is to alter the values of static properties accidentally, and unknowingly; and the danger is that static properties are part of the global state. Public static properties are accessible from anywhere in the application, and can be changed from anywhere within the application. For private or protected static properties in a class, two or more methods within that class probably access them — otherwise they could simply be local-scoped variables if only a single method needed access to them (perhaps keeping them locally static inside that method if their value needs to be maintained between calls, although that too has its own risks, and isn’t something that I would normally recommend) — and any methods in that class are capable of changing their value.
The fact that there are so many pitfalls when using static properties means that they need so much more testing, and yet they are difficult to test for precisely those reasons. Every test should start with a known state, but the global state of static properties can be modified by previous tests, leaving us with an indeterminate state.


The best solution is to avoid using static properties at all, unless you can come up with a very strong justification to do so; but (unless you’re working on a new greenfield application) that isn’t always an option. When you’re working with legacy code, then you may need to make changes to eliminate static properties from your classes.

Where static properties are set initially and never changed, it would probably be better to convert them to class constants if possible: that way avoiding any risk of their value being modified elsewhere in the code. This isn’t always an option if their initial value is dependent on run-time information, even though PHP since 5.6 has supported the use of basic expressions when declaring an initial value for a property.

Alternatively, it might be possible to redefine the static properties as instance properties; but that depends entirely on how they are used within the codebase, and could involve extensive changes across the system as a whole.


If you can’t eliminate static properties from the codebase, then you will certainly have difficulty testing those classes unless you can force resetting the state.

One of the more common methods that I’ve seen people using is to make their static properties public; or to provide public getter and setter methods to access private or protected static properties in a class, just so that we can test them and reset them after each test. The latter option, besides adding bloat to the classes purely for the benefit of making it testable, rather negates the value of declaring the properties with a visibility of protected or private in the first place. And declaring static properties as public, or the existence of a public setter method for a static property might give other developers the impression that the property is intended to be publicly changeable from within code rather than simply for testing.

Personally, I have a small class that allows me to create a snapshot of all static properties in a class, and subsequently to restore those snapshot values at any point.


class snapshot {
    protected $className;
    protected $statics;
    protected $read;
    protected $reset;

    public function __construct($className) {
        $this->className = $className;
        $this->reader();
        $this->capture();

        $restorer = $this->restorer();
        $this->reset = Closure::bind($restorer, null, $this->className);
    }

    protected function reader() {
        $savedStatics = &$this->statics;
        $capturer = function($statics) use (&$savedStatics) {
            foreach($statics as $static) {
                $name = $static->getName();
                $savedStatics[$name] = static::$$name;
            }
        };
        $this->read = Closure::bind($capturer, null, $this->className);
    }

    public function capture() {
        $adjustor = new ReflectionClass($this->className);
        $statics = $adjustor->getProperties(ReflectionProperty::IS_STATIC);

        $fetcher = $this->read;
        $fetcher($statics);
    }

    protected function restorer() {
        return function($statics) {
            foreach($statics as $name => $value) {
                static::$$name = $value;
            }
        };
    }

    public function restore() {
        $reset = $this->reset;
        $reset($this->statics);
    }
}

This means that I don’t need to bloat the class that I’m testing with spurious getter and setter methods in order to reset the static properties. I can use this in the setUp() of a test to take a snapshot that I can then use to reset those static values in the tearDown(), always leaving the values of those static properties in their initial state before the next test.


Statics, whether methods or properties, do cause a lot of difficulties when you’re testing. And just because we can find approaches that will overcome those testing problems, we should still try to avoid using them where we can. There are times and places where it may not be possible to avoid statics; but we should always be aware of the problems and difficulties that they can cause, not simply in testing, but within the application as a whole.

 

Advertisements
This entry was posted in PHP and tagged , , . Bookmark the permalink.

6 Responses to Discharging Static #2

  1. Gabriel O says:

    Do you know if current mocking tools provide the functionality of resetting statics in class, like you have shown in your code snippet? I see this useful, but would like to avoid copying custom code I would need to maintain after.

    Like

    • Mark Baker says:

      I believe that AspectMock offers functionality for monitoring and resetting static properties, but can’t say for certain at the moment… it’s something that I’m currently investigating

      Like

  2. Ondrej Frei says:

    Thanks for an interesting article, Mark! I like the approach of binding closures in your snapshot class.
    One thing – I guess the code in the section “Even if we define the method calls to use static rather than self, so that we’re using late static binding” should use static instead of self.

    Like

    • Mark Baker says:

      Woops! Quite correct. It doesn’t matter how many times I read through that looking for errors, I still missed it

      Like

    • Mark Baker says:

      I find being able to bind closures to a class or object is incredibly powerful, and a technique that I probably overuse; but I think it’s fully justifiable in this case where I only want to apply the method during testing rather than in production code, where a few purists might complain about it

      Like

      • Ondrej Frei says:

        That’s true – I guess some might say it breaks encapsulation and makes the code more difficult to read, but that’s fully justifiable in test environment IMO 🙂

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s