Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Static methods are death to testability" - alternatives for alternative constructors?

It is being said that "static methods are death to testability". If that is so, what is a viable alternative pattern for the below?

class User {

    private $phone,
            $status = 'default',
            $created,
            $modified;

    public function __construct($phone) {
        $this->phone    = $phone;
        $this->created  = new DateTime;
        $this->modified = new DateTime;
    }

    public static function getByPhone(PDO $pdo, $phone) {
        $stmt = $pdo->prepare('SELECT * FROM `users` WHERE `phone` = :phone');
        $stmt->execute(compact('phone'));
        if (!$stmt->rowCount()) {
            return false;
        }

        $record         = $stmt->fetch(PDO::FETCH_ASSOC);
        $user           = new self($record['phone']);
        $user->status   = $record['status'];
        $user->created  = new DateTime($record['created']);
        $user->modified = new DateTime($record['modified']);
        return $user;
    }

    public function save(PDO $pdo) {
        $stmt = $pdo->prepare(
            'INSERT INTO `users` (`phone`, `status`, `created`, `modified`)
                  VALUES         (:phone,  :status,  :created,  :modified)
             ON DUPLICATE KEY UPDATE `status`   = :status,
                                     `modified` = :modified');

        $data = array(
            'phone'    => $this->phone,
            'status'   => $this->status,
            'created'  => $this->created->format('Y-m-d H:i:s'),
            'modified' => date('Y-m-d H:i:s')
        );

        return $stmt->execute($data);
    }

    ...

}

This is just a cut down example. The class has a few more methods and properties and there's more validation when writing to the database etc. The guiding design principle behind this class is that it models a user as an object. Some of the object's properties cannot be modified after it has been created, like the phone number (which acts as a primary id), the date the user was created and so on. Other properties can only be altered according to strict business rules, which all have strictly validating setters and getters.

The object does not represent a database record per se, the database is only seen as one possible form of permanent storage. As such, a database connector is not stored in the object but rather needs to be injected every time the object needs to interact with the database.

When a new user is created, this looks like:

$user = new User('+123456789');

When an existing user is restored from permanent storage, that looks like:

$pdo  = new PDO('...');
$user = User::getByPhone($pdo, '+123456789');

If I were to take the "death to testability" line seriously, this is supposedly bad. I am perfectly able to test this object though, since it is fully dependency injected and the static methods have no state. How could I do this any differently and avoid the use of static methods? Or rather, what exactly argues against static in this case? What makes this particular use of static methods so hard to test?

like image 703
deceze Avatar asked Jan 23 '12 01:01

deceze


People also ask

Why static methods are not testable?

Static methods themselves aren't untestable, but if the object being tested calls a static method then a test cannot "get in between" and make it call a stub method instead. If the object being tested instead calls a regular method, the test can give it an alternative object with a stub implementation of that method.

Do static methods have constructors?

A static constructor is used to initialize any static data, or to perform a particular action that needs to be performed only once. It is called automatically before the first instance is created or any static members are referenced.

What is difference between static method and constructor?

Constructors can't have any return type not even void. Static factory methods can return the same type that implements the method, a subtype, and also primitives. Inside the constructor, we can only perform the initialization of objects.

Why static methods are better?

They are faster — Static methods are slightly faster than instance methods because in instance methods, you are also working with an implicit this parameter. Eliminating that parameter gives a slight performance boost in most programming languages.


2 Answers

This is mostly a summary of (my perspective of) the chat that ensued between me and @zerkms:

The point of contention is actually this:

public function doSomething($id) {
    $user = User::getByPhone($this->pdo, $id);

    // do something with user

    return $someData;
}

This makes it hard to test doSomething since it hardcodes the User class, which may or may not have a lot of dependencies. But this is in fact the same as instantiating the object using a non-static method:

public function doSomething($id) {
    $user = new User;
    $user->initializeFromDb($this->pdo, $id);

    // do something with user

    return $someData;
}

We're not using a static method, but it's still unmockable. Actually, it got worse.
The answer is to use a factory:

public function doSomething($id) {
    $user = $this->UserFactory->byPhone($id);

    // do something with user

    return $someData;
}

Now the factory can be dependency injected and mocked and the User class is no longer hardcoded. You may or may not think this overkill, but it certainly improves mockability.

That does not change the fact though that this factory may very well instantiate the actual user object using a static method:

public function byPhone($id) {
    return User::getByPhone($this->db, $id);
}

There's no difference between using a static method or a regular constructor here.

$user = new User($db, $id);
$user = User::getByPhone($db, $id);

Both expressions return an instance of User and both "hardcode" the User class. Which simply needs to happen at some point anyway.

For my use case, a static constructor method makes the most sense for the object. And as was demonstrated, static methods are not the problem. Where to call them is the point of contention, not that they exist at all. And I have yet to see a convincing argument for not using static constructors, since they can be wrapped in a factory, which alleviates any problem with mockability, the same as it does for regular object instantiation.

like image 187
deceze Avatar answered Oct 13 '22 17:10

deceze


Static methods are only "death to testability" if they depend on state. If you avoid writing such methods to begin with (which you should), then this issue simply goes away.

The Math.abs() example given is one of a good use of a static method. It does not depend on state, therefor it is super easily tested.

That said, whether or not you think static methods should be used is another story. Some people dislike their seemingly procedural nature. I agree with those who say that OOP is a tool, not a goal. If writing "proper" OO code doesn't make sense for a particular situation (e.g. Math.abs()), then don't do it. I promise the boogey man won't eat your application just because you used a static method. :-)

like image 38
FtDRbwLXw6 Avatar answered Oct 13 '22 18:10

FtDRbwLXw6