M
M
Marcuzy2015-09-16 17:36:10
Yii
Marcuzy, 2015-09-16 17:36:10

How to test methods that create objects of other classes?

For example, there is such a class where I need to test the someMethod method

class Foo
{
  public function someMethod()
  {
    $bar = new Bar;
    $bar->method1();
    $bar->method2();
    $blabla = $bar->getResult();
    //etc
  }
}

Internally, it instantiates the Bar class, which can access "slow" data sources, can internally have relationships with other objects whose behavior is difficult to account for by testing just one pathetic method. The only way I know of is to move the creation of an object of the Bar class into a separate method of the Foo class like createBar(array $config), which makes it easy to mock the method when writing a test. Is there another way? In practice, there is often a call to ActiveRecord models, for example, searching by id, etc., it is very inconvenient to test without rewriting the code.

Answer the question

In order to leave comments, you need to log in

3 answer(s)
M
matperez, 2015-09-16
@Marcuzy

Separate the creation of an instance and its use.

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);
  }
}

About what to do with ActiveQuery...
First, take out the query logic in a separate class as well. If you are generating a new model, Gii can do it itself.
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);
    }
}

Pass this object to the target class in the same way as a factory:
class Bar
{
  protected $query;
  public function __construct(FooQuery $query)
 {
    $this->query = $query;
 }

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

Well, then moka as in the first case
$queryMock = \Mockery::mock(FooQuery::class);
$queryMock->shouldRecieve('where->one')->andReturn($fooMock);

ActiveQuery can be partially mocked, after which all native methods will be executed, but saving to the database will be skipped.
$fooMock = \Mockery::mock(Foo::class.'[save, update]');
$fooMock->shouldRecieve('save', 'update')->andReturn(true);

Relations can not be replaced at all. They are perfectly substituted via ActiveRecord::populateRelation() .
$foo = new Foo();
$foo->populateRelation('bar', new Bar());

Q
qRoC, 2015-09-16
@qRoC

Using Dependency Injection solves this problem.

P
Paulus, 2015-09-17
@ppokrovsky

You should not initialize the Bar object in the someMethod method, instead use the Dependency Injection pattern. This way, firstly, fewer dependencies are created, and secondly, such a construction is easier to test: in a unit test, you simply add the Bar mock through injection. I personally prefer constructor injection in Yii2, but that's more of a matter of taste.
As a matter of fact, matperez implemented exactly this pattern.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question