jeudi 21 mars 2019

Singleton patterns vs. unit testing and teardown

We have a CMS framework. This is important in the sense that the framework manages everything concerning a request - from class loading to DB connection management to error handling.

In order to do this, a number of system singletons are used - either system state singletons or factories.

Most singletons are ATM implemented using abstract classes with static vars. At the end of the questions, we post the code exeplifying the pattern (Singleton1) for the classes. Note, none of the singletons in discussion are ever persisted, serialized or otherwise need an actual object instance - our question doesn't regard functionality, they work fine like this.

We are setting in place automatic testing: unit, functional, acceptance. We are using Codeception (which uses PHPUnit), but please take abstraction of the testing platform, let's use it just as example. We ask about generic automated testing at large.

The problem we are facing is supporting decent teardown to test some of the system functions. For instance, it is apparent to us class loading is irreversible because PHP doesn't support unloading classes, so only process isolation provides decent teardown for the class loading (is this true?). However, apart from this, the other singletons could/should be tested.

The abstract class-based singleton pattern seems more awkward to tear down, but PHPUnit makes a decent effort to "rewind" class static properties between tests, so it kindof works. The static properties are saved and restored between tests.

The single-instance singleton (Singleton2 in the code sample below) pattern looks more suited because it allows explicit teardown (just unset the single instance reference). It also would allow, in theory, multiple "context" singletons - so we can preinitialize a number of singletons to be reused for various test contexts.

We see a consistency hazard that would persist in both implementations and that we see no way to wrap around (except process isolation): take a singleton factory for instance. While its state can "apparently" be saved/restored, this is very shallow: the object instances that the factory manipulates could do manipulations on global state that are difficult to restore generically (file locks come to mind as an example).

Questions: What are THE singleton and teardown patterns to be used to make your life easy for automated testing? What are the common advantages and pitfalls of various such patterns?

Given the description (albeit maybe a little brief in terms of describing our patterns) and your experience, do you notice something obvious related to the subject that we are missing out or assuming wrong?

    /*  Pattern 1: abstract singleton; teardown is done 
        by the testing framework by resetting static properties */
    abstract class Singleton1 {
        protected static $singletonProperty1;    // Protected - allow singleton emancipation
        protected static $singletonProperty2;
        public static function init(){
            self::$singletonProperty1='Hello';
            self::$singletonProperty2='World';
        }
        public static function use(){
            echo implode(' ',[self::$singletonProperty1,self::$singletonProperty2]);
        }
    }

    Singleton1::init();
    Singleton1::use();

    /*  Pattern 2: single-instance singleton w/ explicit teardown */
    class Singleton2 {
        private static $instance;

        protected $singletonProperty1;    // Protected - allow singleton emancipation
        protected $singletonProperty2;

        protected function __construct(){
            $this->singletonProperty1='Hello';
            $this->singletonProperty2='World';
        }
        public function use(){
            echo implode(' ',[$this->singletonProperty1,$this->singletonProperty2]);
        }

        public static function getInstance(){
            if(!self::$instance){
                $class=get_called_class();    // Backwards PHP compatibility of "static"
                self::$instance=new $class();
            }
            return self::$instance;
        }
        public static function tearDown(){
            self::$instance=null;    // Backwards PHP compatibility: do NOT use unset on class/object properties, inconsistent behavior
        }
    }

    Singleton2::getInstance()->use();


Aucun commentaire:

Enregistrer un commentaire