解决Laravel中makeWith()无法取到被mock的实例的问题   2019-08-16


被测单元有一行实例化一个类的代码,而且该类的构造方法需要参数。基于面向单元测试编程的原则,通过容器的makeWith()方法实现:

1
$api = app()->makeWith(Api::class, ['config' => $config]);

但是在执行单元测试时发现,虽然测试代码中已经mock了这个类且注入到容器,但在被测单元中取到的还是原类的实例。

实际上,测试代码中在将mock的实例注入容器时使用的是instance()方法:

1
$this->app->instance($class, $mockedObj);

而容器在取带构造参数的类的实例时,并不取通过instance()方法注册进来的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);

$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);

// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}

$this->with[] = $parameters;

$concrete = $this->getConcrete($abstract);

// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}

// ...

}

当存在构造参数时,容器认为是“上下文相关的构造”(needsContextualBuild),所以尝试通过具体的(concrete)逻辑实时构造。

进一步地,getConcrete()方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}

// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically.
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}

return $abstract;
}

它从bindings数组中获取构造逻辑。因此,可以将测试代码中注册被mock的实例的方法改成如下所示:

1
$this->app->offsetSet($class, $mockedObj);

因为offsetSet()方法就是通过bind()方法把被mock的实例注册到容器的:

1
2
3
4
5
6
public function offsetSet($key, $value)
{
$this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
return $value;
});
}

本文基于署名4.0国际许可协议发布,转载请保留本文署名和文章链接。 如您有任何授权方面的协商,请联系我。