Migra a suite para PHPUnit 13, mantendo suporte à 12 - #6
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 usavamwith()foram revistos um a um e viraramatLeastOnce(),once()ounever()conforme o corpo do teste.Um método dublado só aceita uma configuração de
with(), o que inviabiliza o encadeamentoat(N)->method()->with()— a razão de existir do shimat(). O shim passou a receber as expectativas de argumento e as verifica eminvokedDo(), 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 atalhomethod()do PHPUnit —CakeRequestdeclara ummethod()real, que é justamente por que esses casos usam stub escrito à mão. Para eles entrou o matcheranyInvocation(), 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 dotruncate(), masCakeTestFixture::truncate()captura oPDOExceptione devolvefalse. Nunca disparou; a falha só aparecia depois, noinsert(). Derrubava a fixturecore.translateparaBehaviorCollectionTesteTreeBehaviorScopedTest.SecurityComponentTestdeixavaHTTPSligado em$_SERVER, e oCakeSessionpassa a acrescentarsession.cookie_securea todo teste seguinte.CakeRequestTest,CakeResponseTesteRequestHandlerComponentTestlimpavam$_SERVERpor lista curada de chaves, e as listas tinham ficado para trás:HTTP_USER_AGENTeCONTENT_TYPEescapavam. Passam a restaurar o array inteiro.CakeSessionTestnão zeravaCakeSession::$_userAgent, estático que sobrevive aodestroy().CakeEventManagerTestanexava listeners ao manager global sem soltá-los, elisteners()mescla os globais no resultado de qualquer manager local.CakeRequestStubganhouaccepts(),parseAccept()eclientIp(): os testes dublavam esses métodos, mas o stub não os interceptava, então rodavam de verdade contra$_SERVER.Rector removido
rector/rectorestava 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 comrector.phpeRECTOR.md(ophpstanveio junto, era dependência transitiva). As regras que ajudaram na migração vieram de um Rector 2 avulso, fora do projeto.Vale registrar: o
RemoveExpectAnyFromMockRectornão é seguro aqui. Ele removeexpects()cegamente, e nos doubles deCakeStubTraitisso transforma a configuração do mock numa chamada ao acessor real doCakeRequest. Rodar Rector neste repo exige a suíte logo depois.CI
Volta a testar 8.3 e passa a rodar
composer updateem vez deinstall— 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:
--order-by=default--order-by=reverse--order-by=random(seeds 1, 7, 42, 99)Antes das correções de poluição,
reverseerandomquebravam.