Marcuzy
@Marcuzy
php разработчик

Как тестировать методы, создающие объекты других классов?

Например есть такой класс, где мне нужно протестировать метод someMethod
class Foo
{
	public function someMethod()
	{
		$bar = new Bar;
		$bar->method1();
		$bar->method2();
		$blabla = $bar->getResult();
		//etc
	}
}

Внутри он инстанцирует класс Bar, который может обращаться к "медленным" источникам данных, может внтури иметь связи с другими объектами, поведение которых сложно учитывать, тестируя всего лишь один жалкий метод. Я знаю только один способ - вынести создание объекта класса Bar в отдельный метод класса Foo типа createBar(array $config), благодаря чему метод при написании теста лего мокать. Есть ли другой способ? На практике часто присуствует обращение к моделям ActiveRecord, к примеру поиск по id и прочее, очень неудобно тестировать, не переписывая код.
  • Вопрос задан
  • 544 просмотра
Решения вопроса 1
@matperez
Разнесите создание инстанса и его использование.
class BarFabric
{
  public function create(array $config = [])
  {
    return new Bar($config);
  }
}
class Foo
{
  protected $barFabric;
  public function __construct(BarFabric $barFabric)
  {
    $this->barFabric = $barFabric;
  }
  public function someMethod()
  {
    $bar = $this->barFabric->create();
    $bar->method1();
    $bar->method2();
    $blabla = $bar->getResult();
    //etc
  }
}

class FooTest
{
  public function testSomeMethod()
  {
    $bar = \Mokery::mock(Bar::class);
    // ... описание поведения для мока
    $factory = \Mokery::mock(BarFactory::class);
    $factory->shouldReceive('create')->andReturn($bar);
    $foo = new Foo($factory);
    $this->assertSomething($foo);
  }
}


О том, как быть с ActiveQuery...

Во первых, выносите логику запроса так же в отдельный класс. Если генерируете новую модель, Gii это может сделать и сам.
class FooQuery extend ActiveQuery
{
    /**
     * @inheritdoc
     * @return Foo[]|array
     */
    public function all($db = null)
    {
        return parent::all($db);
    }

    /**
     * @inheritdoc
     * @return Foo|array|null
     */
    public function one($db = null)
    {
        return parent::one($db);
    }
}


Этот объект передавайте в целевой класс так же как фабрику:
class Bar
{
  protected $query;
  public function __construct(FooQuery $query)
 {
    $this->query = $query;
 }

 public function someMethod()
 {
    $foo = $this->query->where(...)->one();
    $foo->doSomething();
 }
}


Ну а дальше мокайте как и в первом случае
$queryMock = \Mockery::mock(FooQuery::class);
$queryMock->shouldRecieve('where->one')->andReturn($fooMock);


ActiveQuery можно мокать частично, после этого будут выполняться все родные методы, а вот сохранение в базу пропустится.
$fooMock = \Mockery::mock(Foo::class.'[save, update]');
$fooMock->shouldRecieve('save', 'update')->andReturn(true);


Relations можно не подменять вообще. Они прекрасно подставляются через ActiveRecord::populateRelation().

$foo = new Foo();
$foo->populateRelation('bar', new Bar());
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
ppokrovsky
@ppokrovsky
Не следует инициализировать объект Bar в методе someMethod, вместо этого следует использовать паттерн Dependency Injection. Так во-первых создается меньше зависимостей, во-вторых такую конструкцию проще тестировать: в юнит-тесте Вы просто добавляете мок Bar через инъекцию. Я лично в Yii2 предпочитаю инъекцию через конструктор, но это скорее дело вкуса.

Собственно говоря matperez ровно этот паттерн и реализовал.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы