Skip to content

Migra a suite para PHPUnit 13, mantendo suporte à 12 - #6

Merged
gersonfs merged 8 commits into
masterfrom
phpunit-13
Aug 13, 2026
Merged

Migra a suite para PHPUnit 13, mantendo suporte à 12#6
gersonfs merged 8 commits into
masterfrom
phpunit-13

Conversation

@gersonfs

Copy link
Copy Markdown
Owner

Sobe o PHPUnit para ^12.0 || ^13.0. Em PHP 8.3 o composer resolve a 12 (a 13 exige >= 8.4.1); nas demais versões, a 13.

O que a PHPUnit 13 quebrava

Na primeira execução com a 13: 63 erros, 1 falha e 297 deprecations em 3976 testes.

TestCase::any() virou hard-deprecation (removida na 14) — 136 call sites. Os 121 que só configuravam retorno viraram stub simples ($mock->method()); os 14 que também usavam with() foram revistos um a um e viraram atLeastOnce(), once() ou never() conforme o corpo do teste.

Um método dublado só aceita uma configuração de with(), o que inviabiliza o encadeamento at(N)->method()->with() — a razão de existir do shim at(). O shim passou a receber as expectativas de argumento e as verifica em invokedDo(), que o PHPUnit só alcança depois de o nome do método casar (matches() sozinho dispara para chamadas de outros métodos do mesmo mock). 314 sites convertidos.

Doubles apoiados em CakeStubTrait (CakeRequest/CakeResponse) não expõem o atalho method() do PHPUnit — CakeRequest declara um method() real, que é justamente por que esses casos usam stub escrito à mão. Para eles entrou o matcher anyInvocation(), que exprime "zero ou mais" sem passar pela API deprecada.

Poluição de estado entre testes

Cinco pontos em que um teste deixava estado global sujo. Nenhum quebrava na ordem default, então passavam despercebidos até a suíte ser reordenada.

  • CakeFixtureManager::_setupTable() — o guard que detectaria uma tabela dropada por outra classe esperava uma exceção do truncate(), mas CakeTestFixture::truncate() captura o PDOException e devolve false. Nunca disparou; a falha só aparecia depois, no insert(). Derrubava a fixture core.translate para BehaviorCollectionTest e TreeBehaviorScopedTest.
  • SecurityComponentTest deixava HTTPS ligado em $_SERVER, e o CakeSession passa a acrescentar session.cookie_secure a todo teste seguinte.
  • CakeRequestTest, CakeResponseTest e RequestHandlerComponentTest limpavam $_SERVER por lista curada de chaves, e as listas tinham ficado para trás: HTTP_USER_AGENT e CONTENT_TYPE escapavam. Passam a restaurar o array inteiro.
  • CakeSessionTest não zerava CakeSession::$_userAgent, estático que sobrevive ao destroy().
  • CakeEventManagerTest anexava listeners ao manager global sem soltá-los, e listeners() mescla os globais no resultado de qualquer manager local.

CakeRequestStub ganhou accepts(), parseAccept() e clientIp(): os testes dublavam esses métodos, mas o stub não os interceptava, então rodavam de verdade contra $_SERVER.

Rector removido

rector/rector estava preso na ^0.12.5, de 2021: não executa no PHP 8.5, usa a API antiga de configuração e registra uma regra que não existe mais. Saiu junto com rector.php e RECTOR.md (o phpstan veio junto, era dependência transitiva). As regras que ajudaram na migração vieram de um Rector 2 avulso, fora do projeto.

Vale registrar: o RemoveExpectAnyFromMockRector não é seguro aqui. Ele remove expects() cegamente, e nos doubles de CakeStubTrait isso transforma a configuração do mock numa chamada ao acessor real do CakeRequest. Rodar Rector neste repo exige a suíte logo depois.

CI

Volta a testar 8.3 e passa a rodar composer update em vez de install — o lock fixa uma versão só do PHPUnit, e instalar a partir dele falharia no leg do 8.3. Resolvendo por leg, a matriz acaba cobrindo as duas versões de verdade.

Verificação

3976 testes, sem falhas, sem deprecations do PHPUnit:

PHPUnit 12.5.33 PHPUnit 13.3.0
--order-by=default ok ok
--order-by=reverse ok ok
--order-by=random (seeds 1, 7, 42, 99) ok (99) ok

Antes das correções de poluição, reverse e random quebravam.

PHPUnit 13 exige PHP >= 8.4.1 e endurece a API de test doubles. Três
frentes quebravam a suite:

- TestCase::any() virou hard-deprecation (removida na 14). Os 121 sites
  que só configuravam retorno viraram stub simples ($mock->method()),
  via RemoveExpectAnyFromMockRector. Os 14 que também usavam with()
  foram revistos à mão, virando atLeastOnce()/once()/never() conforme o
  corpo do teste.

- Os doubles apoiados em CakeStubTrait (CakeRequest/CakeResponse) não
  expõem o atalho method() do PHPUnit: CakeRequest declara um method()
  real, que é justamente a razão de esses casos usarem um stub escrito à
  mão em vez de um mock gerado. Para eles ficou expects() com o novo
  matcher anyInvocation(), que exprime "zero ou mais" sem passar pela
  API deprecada.

- Um método dublado aceita uma única configuração de with(), o que
  inviabiliza o encadeamento at(N)->method()->with(). O shim at() passou
  a receber as expectativas de argumento e as verifica em invokedDo(),
  onde o nome do método já casou; 314 sites convertidos.

O CI perde o leg de PHP 8.3 — o framework continua declarando >=8.3, mas
o PHPUnit 13 não instala nessa versão.

Suite: 3976 testes, 0 falhas, 0 deprecations do PHPUnit.
O rector/rector estava preso na ^0.12.5, de 2021: não executa no PHP 8.5,
usa a API antiga de configuração (ContainerConfigurator) e registra o
SpecificAssertContainsRector, que nem existe mais nas versões atuais. As
regras de PHPUnit também migraram para o pacote rector/rector-phpunit
desde então. Sai a dependência junto com rector.php e RECTOR.md; o
phpstan vem junto, era dependência transitiva dele.

O piso do PHP sobe de 8.3 para 8.4 acompanhando o PHPUnit 13, que exige
>= 8.4.1. Sem isso o projeto declararia suporte a uma versão que o CI não
consegue mais testar.
Cinco pontos em que um teste deixava estado global sujo para os
seguintes. Nenhum quebrava na ordem default, então passavam despercebidos
até a suíte ser reordenada — --order-by=reverse e --order-by=random
expõem os cinco.

- CakeFixtureManager::_setupTable() tentava detectar uma tabela dropada
  por outra classe esperando uma exceção do truncate, mas
  CakeTestFixture::truncate() captura o PDOException e devolve false. O
  guard nunca disparava, e a falha só aparecia depois, no insert().
  SchemaShellTest::testCreateNoArgs dropa i18n legitimamente (ela mesma
  criou a tabela), o que derrubava a fixture core.translate para
  BehaviorCollectionTest e TreeBehaviorScopedTest.

- SecurityComponentTest::testRequireSecureSucceed deixava HTTPS ligado em
  $_SERVER. CakeSession lê isso e passa a acrescentar
  session.cookie_secure à config de sessão de todo teste posterior.

- CakeRequestTest, CakeResponseTest e RequestHandlerComponentTest
  limpavam $_SERVER por lista curada de chaves, e as listas tinham
  ficado para trás: HTTP_USER_AGENT e CONTENT_TYPE escapavam. O primeiro
  chega a um branch de CakeResponse::file() e fazia testFileRange contar
  uma chamada extra a type(); o segundo chegava a
  CakeRequestTest::testContentType. As três passam a restaurar o array
  inteiro. Em CakeResponseTest o restore ainda era condicionado a haver
  valor anterior, o que sob CLI nunca há.

- CakeSessionTest não zerava CakeSession::$_userAgent, estático que
  sobrevive ao destroy(). Dois testes o põem em 'bogus!', e o segundo a
  rodar comparava 'bogus!' com 'bogus!' e via sessão válida onde esperava
  divergência.

- CakeEventManagerTest anexava listeners ao manager global sem soltá-los.
  listeners() mescla os globais no resultado de qualquer manager local,
  então sobravam em testAttachListeners.

CakeRequestStub ganhou accepts(), parseAccept() e clientIp(): os testes
dublavam esses métodos, mas o stub não os interceptava, então rodavam de
verdade e liam $_SERVER.

Verificado em --order-by default, reverse e random com as seeds 1, 7, 42
e 99. Antes, reverse e random quebravam.
A constraint passa a ^12.0 || ^13.0 e o piso do PHP volta para 8.3. A 13
exige PHP >= 8.4.1, então em 8.3 o composer resolve a 12; nas demais
versões, a 13.

O CI volta a testar 8.3 e passa a rodar composer update em vez de
install: o lock fixa uma versão só do PHPUnit, e instalar a partir dele
falharia no leg do 8.3. Resolvendo por leg, cada versão de PHP pega o
PHPUnit que lhe serve — e a matriz acaba cobrindo as duas.

Nada no código precisou de adaptação. Os pontos da migração são todos
API que as duas versões têm: o matcher AnyInvokedCount por trás de
anyInvocation(), o hook invokedDo() usado pelo shim at() para conferir
argumentos, e os stubs de retorno, que na 13 mudaram de diretório mas
mantiveram o namespace. O xsi:noNamespaceSchemaLocation volta para 12.5,
o piso suportado; o arquivo valida nas duas e nenhuma reclama.

Suíte verificada nas duas versões, em ordem default, reverse e random
(seed 99): 3976 testes, sem falhas.
Mover o with() para dentro do at() apagou a asserção em dois casos.

Nos doubles apoiados em CakeStubTrait (CakeRequest, CakeResponse) o
matcher nunca chega em invokedDo(): CakeStubTrait intercepta a chamada e
CakeStubBuilder::_cakeVerifyIndexed() confere os argumentos depois, lendo
só o que veio de with(). Sem with(), _with ficava null e a verificação
retornava cedo — dezenas de asserções de header em CakeResponseTest,
além de RequestHandlerComponentTest, ControllerTest e AuthComponentTest,
seguiam verdes com Content-Type errado ou status trocado. O
CakeStubBuilder passa a ler os argumentos do próprio matcher, via um
getter novo no shim; um with() explícito ainda tem precedência. Só
CakeResponseTest sai de 407 para 595 asserções.

Em ModelTaskTest::testInteractiveFieldValidationWithBogusResponse o
with() sumiu por outro motivo: a primeira versão do script de conversão
casava `$this->at()` dentro de docblocks, e num deles moveu para o texto
do comentário o with() da instrução seguinte. O comentário foi corrigido
na hora, o with() não.

Auditei todos os arquivos convertidos comparando com o master: os únicos
outros déficits de with() são os dois merges em logicalAnd e a troca por
never() no SmtpTransportTest, ambos deliberados.

Verificado por mutação — trocar um Content-Length esperado ou o texto da
mensagem agora faz o teste falhar, e antes não fazia. Suíte verde nas
duas versões do PHPUnit, em ordem default e reverse.
CakeStubBuilder::_cakeVerify() pulava a checagem de argumentos quando o
matcher era AnyInvokedCount ou InvokedAtMostCount, alegando que o PHPUnit
faz o mesmo. Não faz: Matcher::invoked() roda parametersRule->apply()
logo depois da regra de contagem, seja qual for o matcher — confirmado
por probe, um with() violado sob any() e sob atMost(3) falha nos dois.

Na prática isso desativava em silêncio o with() dos doubles construídos
com anyInvocation(). Hoje há um único call site nessa forma
(MediaViewTest), mas o buraco valia para qualquer um que aparecesse.

never() continua coberto pela guarda de contagem zero: se houvesse
chamada, a asserção de contagem acima já teria falhado. atMost() não é
usado em lugar nenhum da suíte.

Suíte verde nas duas versões do PHPUnit, em default e reverse.
Minor, não patch: a árvore de testes distribuída ganhou API pública —
CakeTestCase::anyInvocation(), um segundo parâmetro opcional em at() e
accepts()/parseAccept()/clientIp() no CakeRequestStub. Junto vai uma
mudança de comportamento que pode deixar vermelha a suíte de quem
consome o framework: CakeStubBuilder passou a conferir os argumentos de
with() também sob any(), o que antes era silenciosamente ignorado.

Nenhum código de runtime mudou — o diff fora de lib/Cake/Test/Case fica
todo em lib/Cake/TestSuite, mais CI e dependências de desenvolvimento.

O 2.11.0 estava livre: foi bumpado em 60703e5 e desfeito em c575cbc, sem
tag em nenhum momento.
Isto é uma biblioteca: quem consome resolve as dependências no próprio
lock, e o daqui só valia para o desenvolvimento do framework. Versioná-lo
fixava uma versão única do PHPUnit para todos os legs do CI, sendo que a
13 não instala em PHP 8.3 — foi por isso que o pipeline teve de trocar
install por update.

O update continua, agora por outro motivo: sem lock, o install apenas
avisa "No composer.lock file present" e resolve as dependências do mesmo
jeito, então update é o comando honesto. Cada leg resolve a sua versão —
8.3 pega o PHPUnit 12, os demais o 13.
@gersonfs
gersonfs merged commit c98ee42 into master Aug 13, 2026
6 checks passed
@gersonfs
gersonfs deleted the phpunit-13 branch August 13, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant