Использование фиктивных объектов в PHP внутри функций, которые создают свои собственные объекты - PullRequest
4 голосов
/ 12 марта 2011

Я искал, как добавить покрытие модульного тестирования в большую существующую кодовую базу, написанную на PHP.Многие функции как в статических, так и в инстанцируемых классах делают вызов библиотеки или создают объект для получения соединений с memcache и базой данных.Обычно они выглядят примерно так:

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = get_memcache();

    $results = $cache->get($key);
    if (!$results) {
        $database = new DatabaseObject();
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

Мои коллеги и я в настоящее время пытаемся реализовать покрытие с помощью PHPUnit для нескольких новых классов, которые мы пишем.Я попытался найти способ создания модульных тестов изолированным способом для функций в нашей существующей кодовой базе, которые напоминают приведенный выше псевдокод, но безуспешно.

Примеры, которые я видел в документации PHPUnitвсе полагаются на наличие в классе некоторого метода, с помощью которого к нему может быть присоединен фиктивный объект, например: $objectBeingTested->attach($mockObject); Я посмотрел на SimpleUnit и увидел там то же самое, фиктивные объекты передавались в класс через его конструктор,Это не оставляет много места для функций, которые создают свои собственные объекты базы данных.

Есть ли способ высмеивать подобные вызовы?Есть ли еще одна система модульного тестирования, которую мы можем использовать?Или нам придется изменить шаблоны, которые мы будем использовать в будущем, чтобы упростить модульное тестирование?

Что я хотел бы сделать, так это иметь возможность поменять весь класс на фиктивный класс, когдатекущие тесты.Например, класс DatabaseObject может быть заменен на фиктивный класс, и каждый раз, когда он создается во время теста, он фактически является экземпляром фиктивной версии.

В моей команде обсуждались вопросы рефакторинга нашегометоды доступа к базе данных и memcache в новом коде, возможно, с помощью синглетонов.Я полагаю, это могло бы помочь, если бы мы написали синглтон таким образом, чтобы его собственный экземпляр мог быть заменен фиктивным объектом ...

Это мой первый опыт модульного тестирования.Если я делаю это неправильно, скажите, пожалуйста.:)

Спасибо.

Ответы [ 4 ]

12 голосов
/ 12 марта 2011

Просто добавьте к ответу @Ezku (+1, все, что я бы сказал тоже), чтобы окончательный код мог выглядеть примерно так (используя Внедрение зависимости )

public function __construct(Memcached $mem, DatabaseObject $db) {
    $this->mem = $mem;
    $this->db = $db;
}

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->mem;

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->db;
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

С этим действительно легко создавать фиктивные объекты и передавать их в код.

Есть несколько причин, по которым вы можете захотеть сделать это (кроме создания тестируемого кода). На этот раз это делает ваш код гораздо более открытым для изменения (требуется другой db? Pass в другом объекте db вместо изменения кода в вашем DatabaseObject.

В этом блоге рассказывается о том, почему статические методы плохи, но использование оператора "new" в вашем коде - это почти то же самое, что сказать $x = StaticStuff::getObject();, поэтому оно применимо и здесь.

Другая ссылка может быть такой: Почему синглтоны плохо подходят для тестируемого кода , потому что он затрагивает те же точки.

Если у вас уже написано больше кода, есть несколько способов реализовать эту идею, не меняя все сразу.

Необязательное внедрение зависимости, например:

public function __construct(Memcached $mem = null, DatabaseObject $db = null) {
    if($mem === null) { $mem = new DefaultCacheStuff(); }
    if($db === null) { $db = new DefaultDbStuff(); }
    $this->mem = $mem;
    $this->db = $db;
}

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->mem;

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->db;
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

или с использованием «инъекции в сеттер»:

public function __construct(Memcached $mem = null, DatabaseObject $db = null) {
    $this->mem = new DefaultCacheStuff();
    $this->db = new DefaultDbStuff();
}

public function setDatabaseObject(DatabaseObject $db) { 
    $this->db = $db;
}

public function setDatabaseObject(Memcached $mem) { 
    $this->mem = $mem;
}

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->mem;

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->db;
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

Кроме того, есть вещи, называемые dependency injection containers, которые позволяют убрать все создаваемые возражения и вытащить все из этого контейнера, но поскольку это делает тестирование немного сложнее (imho), и это поможет вам только в том случае, если все сделано действительно хорошо. не предложил бы начинать с одного, а просто использовать обычное «внедрение зависимостей» для создания тестируемого кода.

7 голосов
/ 12 марта 2011

Это не оставляет много места для функций, которые создают свои собственные объекты базы данных.

Именно так.Вы описываете стиль программирования, который следует избегать именно потому, что он ведет к непроверяемому коду.Если ваш код явно зависит от некоторых внешних факторов и никоим образом не абстрагируется от них, вы сможете протестировать этот код только с неповрежденными этими внешними элементами.Как вы говорите, вы не можете высмеивать вещи, которые функции создают для себя.

Чтобы сделать ваш код тестируемым, предпочтительнее применить внедрение зависимостей: передать зависимости, которые вы хотите, чтобы они были смоделированы, в контекст модуля извне,Обычно это обычно приводит к лучшему дизайну классов.

Тем не менее, есть некоторые вещи, которые вы можете сделать, чтобы включить mockability без явного внедрения: используя средства фиктивных объектов PHPUnit, вы можете переопределить методы даже втестируемое устройство.Рассмотрим рефакторинг, подобный этому.

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->getMemcache();

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->getDatabaseObject();
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

public function getMemcache() {
    return get_memcache();
}

public function getDatabaseObject() {
    return new DatabaseObject();
}

Теперь, если вы тестируете getSomeData (), вы можете смоделировать getMemcache () и getDatabaseObject ().Следующим шагом рефакторинга будет внедрение объектов memcache и database в класс, чтобы у него не было явных зависимостей от get_memcache () или класса DatabaseObject.Это избавит от необходимости применения методов насмешки в самом тестируемом устройстве.

5 голосов
/ 13 марта 2011

В идеальном мире у вас будет время для рефакторинга всего вашего унаследованного кода с использованием внедрения зависимостей или чего-то подобного. Но в реальном мире вам часто приходится сдавать руку, с которой вы получили.

Себастьян Бергманн, автор PHPUnit, написал расширение помощников по тестам , которое позволяет вам переопределить новый оператор с помощью функций обратного вызова и переименования. Это позволит вам обезопасить ваш код во время тестирования, пока вы не сможете изменить его, чтобы сделать его более тестируемым. Конечно, чем больше тестов вы пишете, тем больше работы вам придется отменить.

Примечание: расширение Test-Helper заменено на https://github.com/krakjoe/uopz

0 голосов
/ 25 августа 2016

Я бы предложил очень простой инжектор зависимостей.Они могут быть очень просты в использовании для новых функций внутри унаследованного кода.Также вы можете легко изменить код, который вы опубликовали.

Я предлагаю простой код, подобный тому, который я недавно разработал для аналогичного случая: https://packagist.org/packages/tflori/dependency-injector

В каком-то файле начальной загрузки или файле конфигурации вы что-то пишетекак это:

<?php

DI::set('database', function() { return new DatabaseObject(); });
DI::set('memcache', function() { return get_memcache(); });

И тогда ваша функция может выглядеть так:

<?php

function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = DI::get('memcache');

    $results = $cache->get($key);
    if (!$results) {
        $database = DI::get('database');
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

Чтобы проверить код, вы можете написать testClass, например:

<?php

use PHPUnit\Framework\TestCase;

class GetSomeDataTest extends TestCase {
    public function tearDown() {
        Mockery::close();
        parent::tearDown();
    }

    public function testReturnsCached() {
        $mock = Mockery::mock('memcache_class');
        $mock->shouldReceive('get')->once()->with('SomeMemcacheKey')->andReturn('anyResult');
        DI::set('memcache', $mock);

        $result = getSomeData();

        $this->assertSame('anyResult', $result);
    }

    public function testQueriesDatabase() {
        $memcache = Mockery::mock('memcache_class');
        $memcache->shouldReceive('get')->andReturn(null);
        $memcache->shouldIgnoreMissing();
        DI::set('memcache', $memcache);

        $database = Mockery::mock(DatabaseObject::class);
        $database->shouldReceive('query')->once()->andReturn('fooBar');
        DI::set('database', $database);

        $result = getSomeData();

        $this->assertSame('fooBar', $result);
    }
}
...