Laravel测试断言失败但Seeders运行正常问题排查
问题分析与解决方案
嘿,我帮你找到了问题的根源——这跟函数执行速度完全没关系,问题出在Eloquent的集合缓存机制上!先给你拆解清楚,再给你几个可行的修复方案。
问题回顾
你遇到的情况是:测试套件里的断言总是失败,但同样的逻辑在Seeders里运行正常;第一次执行Seed时会失败,第二次执行时原有数据集没问题,只有新增的数据集会出问题。
相关代码片段
Feature 测试代码
/** * Test if User can follow an other user and send a notification * * @return void */ public function test_user_can_follow_an_other_user() { $follower = factory(User::class)->create(); $followed = factory(User::class)->create(); $this->actingAs($follower, 'web') ->post('/following', ['user' => $followed->id]) ->assertRedirect('/following'); $this->assertTrue($follower->follows($followed)); }
PHPUnit 执行结果
PHPUnit 7.1.5 by Sebastian Bergmann and contributors. .mpoo.F 3 / 3 (100%) Time: 2.91 seconds, Memory: 50.00MB There was 1 failure: 1) Tests\Feature\UserTest::test_user_can_follow_an_other_user Failed asserting that false is true. /Users/gtr/Code/La-dechetterie-du-web/lddw-api/tests/Feature/UserTest.php:69 FAILURES! Tests: 3, Assertions: 9, Failures: 1.
Seeder 代码
public function run() { $follower = User::first(); $following = User::all(); $followingArrayIds = []; foreach ($following as $u) { $follower->follow($u); array_push($followingArrayIds, [ 'follower' => $follower->id, 'pretend_to_follow' => $u->id, 'is '. $follower->id .' following '. $u->id .' ?' => $follower->follows($u) // should be true !!!!! ]); } print_r($followingArrayIds); }
Model 核心函数
/** * User's following relation * * @return Relation User */ public function following(){ return $this->belongsToMany('App\User', 'followers', 'follower_id', 'following_id'); } /** * Follow an other user * * @param User $user * @return void */ public function follow(User $user){ if($this->follows($user)) return; $this->following()->attach($user); } /** * Check if the current user follows the user he wanted to * * @param User $user * @return boolean */ public function follows(User $user){ return $this->following->contains($user); }
问题核心:Eloquent的集合缓存陷阱
当你调用$this->following(注意是属性,不是带括号的方法following())时,Eloquent会做两件事:
- 立即向数据库发起查询,获取当前用户的所有关注列表
- 将查询结果缓存到当前模型实例的
following属性里,后续再调用$this->following时,直接返回缓存的集合,不会再查数据库
回到你的测试场景:
- 你创建了
$follower和$followed两个用户实例 - 通过HTTP请求调用
follow()方法,成功在数据库中添加了关注关系 - 但测试代码里的
$follower实例还是最初创建的那个,它的following集合依然是缓存的空集合(因为请求是在独立的容器中运行,不会更新你本地的实例) - 所以
follows()方法检查缓存集合,返回false,导致断言失败
至于Seeder里的奇怪表现:
- 第一次执行时,
$follower的following集合初始为空,调用follow()后缓存集合没更新,所以follows()返回false - 第二次执行时,原有用户的
following集合是从数据库重新加载的(包含之前的关系),所以检查旧用户时返回true;但新增用户的集合还是空的,所以只有新增的会失败——完全符合集合缓存的表现
修复方案(按推荐程度排序)
方案1:重新从数据库获取用户实例(最稳妥)
在断言前,用fresh()方法重新查询用户,这样会加载最新的数据库数据,覆盖缓存的集合:
$this->actingAs($follower, 'web') ->post('/following', ['user' => $followed->id]) ->assertRedirect('/following'); // 重新获取最新的用户数据 $follower = $follower->fresh(); $this->assertTrue($follower->follows($followed));
方案2:修改follows()方法,直接查询数据库
把follows()改成用关系方法查询,这样每次都会直接访问数据库,绕过集合缓存:
public function follows(User $user){ // 用following()方法(关系查询)替代following属性(缓存集合) return $this->following()->where('following_id', $user->id)->exists(); }
这种方式不需要担心缓存问题,唯一的小缺点是每次调用都会发起一次数据库查询,但对于测试和绝大多数业务场景来说,性能完全够用。
方案3:在follow()方法里手动更新缓存集合
如果你想继续使用集合的方式,可以在attach()之后,把用户手动添加到缓存的集合里:
public function follow(User $user){ if($this->follows($user)) return; $this->following()->attach($user); // 手动更新缓存的following集合 $this->following->push($user); }
注意:如果$this->following还没被访问过(即缓存集合未初始化),这会先触发一次数据库查询,再添加用户,有额外的查询开销,所以不如前两种方案稳妥。
内容的提问来源于stack exchange,提问作者Daedelus
相关产品推荐
相关产品推荐

