test: remove the two real sources of full-run flakiness, and the mock notices

createUser() drew a random mobile and recovered from a collision by catching
the unique-constraint violation and calling resetManager(). That hands back a
brand-new EntityManager, which detaches every entity the running test had
built so far; its next flush died with "Multiple non-persisted new entities
were found", always in a different test and never reproducible in isolation.
The number is now checked before the insert, so the collision never reaches
the database and the manager stays open.

testParentIdAddsNoQueryPerSpecialty counted queries on the first request of
each size, so one-shot per-process caches — site config, subscription plan,
Doctrine metadata — landed inside the count or not depending on which tests
had run before it. Both requests are now warmed first; the assertion measures
steady-state growth, which is what it was always about.

The 23 PHPUnit notices were all one complaint: doubles created with
createMock() that never had an expectation. The ones that only stub return
values became createStub(); in SmsServiceLookupOnlyTest the provider and the
bus got the expectations they were missing, since "dispatch does not touch
the provider" and "sendNow does not enqueue" are exactly what that suite is
there to prove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-18 09:57:29 +03:30
co-authored by Claude Opus 5
parent ed2d2ec74a
commit 8a43297e24
8 changed files with 59 additions and 48 deletions
+15 -20
View File
@@ -11,7 +11,6 @@ use App\Doctor\Entity\Doctor;
use App\Payment\Entity\Payment;
use App\Shared\Context\EntityContext;
use App\Subscription\Entity\SubscriptionPlan;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
@@ -126,27 +125,23 @@ abstract class ApiTestCase extends WebTestCase
}
// 9 random digits after 09 (full ^09\d{9}$ space). db_test is never reset and
// already holds tens of thousands of users, so a draw does collide now and
// then; retry rather than fail an unrelated test on a birthday collision.
// already holds tens of thousands of users, so a draw does collide now and then.
//
// شماره پیش از insert بررسی می‌شود، نه بعد از شکستنِ قید یکتا. برخوردِ واقعی
// EntityManager را می‌بست و راه ترمیمش `resetManager()` بود — که مدیر تازه‌ای
// می‌سازد و همهٔ موجودیت‌هایی را که تستِ جاری تا آن لحظه ساخته بود detach
// می‌کند. اولین flush بعدی با «Multiple non-persisted new entities were found»
// می‌ترکید؛ همان خطای تصادفی که هر بار روی تستِ دیگری می‌افتاد و در اجرای تکی
// هرگز تکرار نمی‌شد. حالا برخورد اصلاً به دیتابیس نمی‌رسد.
for ($attempt = 0; ; $attempt++) {
try {
return $this->persistUser(
'09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT),
$roles,
);
} catch (UniqueConstraintViolationException $e) {
if ($attempt >= 4) {
throw $e;
}
$mobile = '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
// The failed INSERT closes the EntityManager, and asking the container
// for it again hands back the *same closed instance* — Doctrine only
// builds a fresh one when the registry is reset. Without this the retry
// throws EntityManagerClosed, and every later test in the same process
// inherits a dead manager: the intermittent, always-somewhere-else
// failure that made full runs flaky.
static::getContainer()->get('doctrine')->resetManager();
$this->em = static::getContainer()->get(EntityManagerInterface::class);
if ($this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]) === null) {
return $this->persistUser($mobile, $roles);
}
if ($attempt >= 20) {
throw new \RuntimeException('could not draw a free test mobile number after 20 attempts');
}
}
}