扩展 Mockery::on断言以适用于常见用例

huangapple go评论79阅读模式
英文:

Extend Mockery::on assertions for common Use Cases

问题

Is extending Mockery for commonly used assertions possible?

断言模拟函数接收类的实例并匹配标识符是否可能。

Mockery::on(
  fn ($arg) => $arg instanceof App\Models\User && $arg->id === $user->id
)

Is it possible to...

  1. Simplify this call by extending Mockery (via a Macro?) so that this assertion could be Mockery::onUser($user) or Mockery::onUserOfId($id)?

  2. Pass in multiple arguments like Mockery::onUserOfRole($id, $user->role)?

Note: the $user object instance provided to the Mockery::on call in the Test is not the same instance as is passed to the mocked function being tested, hence checking the type and matching the identifier.

Possible Solution ... that's not flexible

A possible solution we have explored is extending the base TestCase class with a matchModel() function, but this extension is not "elegant". I have considered breaking it out into a Trait, but attaching these functions to all Test Cases, $this->onUserOfRole() seems wrong. The solution also fails or needs to be customized. (Illuminate\Database\Eloquent\Model@is() works, but...) if the Object is not a Laravel Model, then we're back to square one:

# Possible Solution that is not extensible and only works on Models
class TestCase extends Illuminate\Foundation\Testing\TestCase
    protected function matchModel(Model $model)
    {
        return new MockeryClosure(function ($arg) use ($model) {
            if ($model->is($arg)) {
                return true;
            }

            // print custom error message

            return false;
        });
    }
}
英文:

Is extending Mockery for commonly used assertions possible?

Asserting that a mocked function receives an instance of a class AND matching an identifier is cumbersome.

Mockery::on(
  fn ($arg) => $arg instanceof App\Models\User && $arg->id === $user->id
)

Is it possible to...

  1. Simplify this call by extending Mockery (via a Macro?) so that this assertion could be Mockery::onUser($user) or Mockery::onUserOfId($id)?

  2. Pass in multiple arguments like Mockery::onUserOfRole($id, $user->role)?

Note: the $user object instance provided to the Mockery::on call in the Test is not the same instance as is passed to the mocked function being tested, hence checking the type and matching the identifier.

Possible Solution ... that's not flexable

A possible solution we have explored is extending the base TestCase class with a matchModel() function, but this extension is not "elegant". I have considered breaking it out into a Trait, but attaching these functions to all Test Cases, $this->onUserOfRole() seems wrong. The solution also fails or needs to be customized. (Illuminate\Database\Eloquent\Model@is() works, but...) if the Object is not a Laravel Model, then we're back to square one:

# Possible Solution that is not extensible and only works on Models
class TestCase extends Illuminate\Foundation\Testing\TestCase
    protected function matchModel(Model $model)
    {
        return new MockeryClosure(function ($arg) use ($model) {
            if ($model->is($arg)) {
                return true;
            }

            // print custom error message

            return false;
        });
    }
}

答案1

得分: 1

The Mockery::on method 是对\Mockery\Matcher\Closure构造函数的一行包装,该类仅定义了两部分自定义逻辑:

  • match方法接受要检查的参数,并返回true或false
  • __toString方法返回固定字符串'<Closure===true>',可能用于错误/调试消息中

实际闭包并不直接定义为属性,它依赖于从MatcherAbstract继承的构造函数,该构造函数设置了一个无类型属性$_expected,但不直接执行其他逻辑。

因此,用于您的用例的替代方法可以简单地重写该构造函数,并使用它们在match方法中实现您的测试,例如:

class MyMatcher extends Mockery\Matcher\MatcherAbstract
{
    private string $expectedClass;
    private string $keyName;
    private mixed $keyValue;

    public function __construct(string $expectedClass, string $keyName, mixed $keyValue)
    {
        $this->expectedClass = $expectedClass;
        $this->keyName = $keyName;
        $this->keyValue = $keyValue;
    }

    /**
     * 检查实际值是否与期望值匹配。
     *
     * @param mixed $actual
     * @return bool
     */
    public function match(&$actual)
    {
        return
            is_a($actual, $this->expectedClass, true)
            &&
            $actual->{$this->keyProperty} === $this->keyValue;
    }

    /**
     * 返回此匹配器的字符串表示
     *
     * @return string
     */
    public function __toString()
    {
        return "<Is {$this->expectedClass} and has {$this->keyProperty} of {$this->keyValue}>";
    }
}

然后这个:

Mockery::on(
  fn ($arg) => $arg instanceof App\Models\User && $arg->id === $user->id
)

变成这样:

new MyMatcher(App\Models\User::class, 'id', $user->id)

如果您希望进一步缩短new MyMatcher,您可以在任何地方放置一个函数,将其包装在与Mockery::on包装new \Mockery\Matcher\Closure相同的函数中。

如果您需要更复杂或可配置的测试,您只需找出一种方法来从match中返回布尔结果。

英文:

The Mockery::on method is just a one-line wrapper around the constructor for the \Mockery\Matcher\Closure class, which in turn defines only two pieces of custom logic:

  • a match method which takes the parameter being examined, and returns true or false
  • a __toString method returning the fixed string &#39;&lt;Closure===true&gt;&#39;, presumably for use in error/debug messages

The actual closure isn't defined directly as a property, it just relies on the constructor inherited from MatcherAbstract which sets an untyped property $_expected, but performs no other logic directly.

So a replacement for your use case can simply over-ride that constructor with whatever arguments you want, and use them in the match method to implement your test, e.g.

class MyMatcher extends Mockery\Matcher\MatcherAbstract
{
    private string $expectedClass;
    private string $keyName;
    private mixed $keyValue;

    public function __construct(string $expectedClass, string $keyName, mixed $keyValue)
    {
        $this-&gt;expectedClass = $expectedClass;
        $this-&gt;keyName = $keyName;
        $this-&gt;keyValue = $keyValue;
    }

    /**
     * Check if the actual value matches the expected.
     *
     * @param mixed $actual
     * @return bool
     */
    public function match(&amp;$actual)
    {
        return
            is_a($actual, $this-&gt;expectedClass, true)
            &amp;&amp; 
            $actual-&gt;{$this-&gt;keyProperty} === $this-&gt;keyValue;
    }

    /**
     * Return a string representation of this Matcher
     *
     * @return string
     */
    public function __toString()
    {
        return &quot;&lt;Is {$this-&gt;expectedClass} and has {$this-&gt;keyProperty} of {$this-&gt;keyValue}&gt;&quot;;
    }
}

Then this:

Mockery::on(
  fn ($arg) =&gt; $arg instanceof App\Models\User &amp;&amp; $arg-&gt;id === $user-&gt;id
)

Becomes this:

new MyMatcher(App\Models\User::class, &#39;id&#39;, $user-&gt;id)

If you want to shorten the new MyMatcher further, you can put a function wherever you like that wraps it in the same Mockery::on wraps new \Mockery\Matcher\Closure.

If you want more complex or configurable tests, all you need to figure out is a way to return a boolean result from match.

huangapple
  • 本文由 发表于 2023年3月20日 23:49:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/75792471.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定