From ff11af33bbfdf742e1e6631f46e0d34cfc2e9ccf Mon Sep 17 00:00:00 2001
From: Oliver Klee <typo3-coding@oliverklee.de>
Date: Mon, 3 Feb 2025 15:45:23 +0100
Subject: [PATCH] [TASK] Upgrade to PHPUnit 10.x

Also drop redundant configuration options.

Fixes #1574
---
 .github/dependabot.yml                        |   2 +-
 Build/php-cs-fixer/php-cs-fixer.php           |   6 -
 Build/phpunit/FunctionalTests.xml             |  24 +---
 Build/phpunit/UnitTests.xml                   |  26 +----
 .../Command/CreateTestDataCommandTest.php     |  34 ++----
 .../Controller/TeaControllerTest.php          |  15 +--
 .../Domain/Repository/TeaRepositoryTest.php   |  60 +++-------
 .../FrontEndEditorControllerTest.php          | 105 +++++-------------
 Tests/Unit/Controller/TeaControllerTest.php   |  29 ++---
 Tests/Unit/Domain/Model/TeaTest.php           |  42 ++-----
 composer.json                                 |   4 +-
 phive.xml                                     |   2 +-
 rector.php                                    |   8 +-
 13 files changed, 93 insertions(+), 264 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 21f649d..b3b9570 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -18,9 +18,9 @@ updates:
     allow:
       - dependency-type: "development"
     ignore:
+      - dependency-name: "brianium/paratest"
       - dependency-name: "doctrine/dbal"
       - dependency-name: "phpunit/phpunit"
-        versions: [ "^10.0" ]
       - dependency-name: "symfony/console"
       - dependency-name: "symfony/translation"
       - dependency-name: "symfony/yaml"
diff --git a/Build/php-cs-fixer/php-cs-fixer.php b/Build/php-cs-fixer/php-cs-fixer.php
index 8281bf8..59c4a30 100644
--- a/Build/php-cs-fixer/php-cs-fixer.php
+++ b/Build/php-cs-fixer/php-cs-fixer.php
@@ -5,12 +5,6 @@ use TYPO3\CodingStandards\CsFixerConfig;
 
 $config = CsFixerConfig::create();
 
-$config->addRules([
-    // This is required as long as we are on PHPUnit 9.x. It can be removed after the switch to PHPUnit 10.x.
-    // @see https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/8337
-    'php_unit_test_case_static_method_calls' => ['call_type' => 'self', 'methods' => ['createStub' => 'this']],
-]);
-
 // @TODO 4.0 no need to call this manually
 $config->setParallelConfig(ParallelConfigFactory::detect());
 
diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml
index f2da77f..7c953b8 100644
--- a/Build/phpunit/FunctionalTests.xml
+++ b/Build/phpunit/FunctionalTests.xml
@@ -1,38 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
+    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
     backupGlobals="true"
     beStrictAboutTestsThatDoNotTestAnything="false"
     bootstrap="../../.Build/vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
     cacheResult="false"
     colors="true"
-    convertDeprecationsToExceptions="true"
-    convertErrorsToExceptions="true"
-    convertNoticesToExceptions="true"
-    convertWarningsToExceptions="true"
     failOnRisky="true"
     failOnWarning="true"
-    forceCoversAnnotation="false"
-    stopOnError="false"
-    stopOnFailure="false"
-    stopOnIncomplete="false"
-    stopOnSkipped="false"
-    verbose="false"
 >
-    <coverage/>
-    <testsuites>
-        <testsuite name="Functional tests">
-            <!--
-                This path either needs an adaption in extensions, or an extension's
-                test location path needs to be given to phpunit.
-            -->
-            <directory suffix="Test.php">./</directory>
-        </testsuite>
-    </testsuites>
     <php>
-        <!-- @deprecated: will be removed with next major version, constant TYPO3_MODE is deprecated -->
-        <const name="TYPO3_MODE" value="BE"/>
         <!--
             @deprecated: Set this to not suppress warnings, notices and deprecations in functional tests
                          with TYPO3 core v11 and up.
diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml
index 7300036..284eb80 100644
--- a/Build/phpunit/UnitTests.xml
+++ b/Build/phpunit/UnitTests.xml
@@ -1,37 +1,15 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
+    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
     beStrictAboutTestsThatDoNotTestAnything="false"
+    bootstrap="../../.Build/vendor/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php"
     cacheResult="false"
     colors="true"
-    convertDeprecationsToExceptions="true"
-    convertErrorsToExceptions="true"
-    convertNoticesToExceptions="true"
-    convertWarningsToExceptions="true"
     failOnRisky="true"
     failOnWarning="true"
-    forceCoversAnnotation="false"
-    processIsolation="false"
-    stopOnError="false"
-    stopOnFailure="false"
-    stopOnIncomplete="false"
-    stopOnSkipped="false"
-    verbose="false"
 >
-    <coverage/>
-    <testsuites>
-        <testsuite name="Unit tests">
-            <!--
-                This path either needs an adaption in extensions, or an extension's
-                test location path needs to be given to phpunit.
-            -->
-            <directory suffix="Test.php">./</directory>
-        </testsuite>
-    </testsuites>
     <php>
-        <!-- @deprecated: will be removed with next major version, constant TYPO3_MODE is deprecated -->
-        <const name="TYPO3_MODE" value="BE"/>
         <ini name="display_errors" value="1"/>
         <env name="TYPO3_CONTEXT" value="Testing"/>
     </php>
diff --git a/Tests/Functional/Command/CreateTestDataCommandTest.php b/Tests/Functional/Command/CreateTestDataCommandTest.php
index 00c558c..ec94f79 100644
--- a/Tests/Functional/Command/CreateTestDataCommandTest.php
+++ b/Tests/Functional/Command/CreateTestDataCommandTest.php
@@ -4,15 +4,15 @@ declare(strict_types=1);
 
 namespace TTN\Tea\Tests\Functional\Command;
 
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
 use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Tester\CommandTester;
 use TTN\Tea\Command\CreateTestDataCommand;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
-/**
- * @covers \TTN\Tea\Command\CreateTestDataCommand
- */
+#[CoversClass(CreateTestDataCommand::class)]
 final class CreateTestDataCommandTest extends FunctionalTestCase
 {
     /**
@@ -38,35 +38,27 @@ final class CreateTestDataCommandTest extends FunctionalTestCase
         $this->commandTester = new CommandTester($command);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function isConsoleCommand(): void
     {
         self::assertInstanceOf(Command::class, $this->subject);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function hasDescription(): void
     {
         $expected = 'Create test data for the tea extension in an already existing page (sysfolder).';
         self::assertSame($expected, $this->subject->getHelp());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function hasHelpText(): void
     {
         $expected = 'Create test data for the tea extension in an already existing page (sysfolder).';
         self::assertSame($expected, $this->subject->getHelp());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function runReturnsSuccessStatus(): void
     {
         $result = $this->commandTester->execute(
@@ -78,9 +70,7 @@ final class CreateTestDataCommandTest extends FunctionalTestCase
         self::assertSame(Command::SUCCESS, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function createsTestData(): void
     {
         $this->commandTester->execute([
@@ -90,9 +80,7 @@ final class CreateTestDataCommandTest extends FunctionalTestCase
         $this->assertCSVDataSet(__DIR__ . '/Fixtures/Database/Teas.csv');
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function deletesExistingDataOnGivenPidBeforeCreatingNewData(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/Database/ExistingTeas.csv');
@@ -106,9 +94,7 @@ final class CreateTestDataCommandTest extends FunctionalTestCase
         $this->assertCSVDataSet(__DIR__ . '/Fixtures/Database/TeasAfterDelete.csv');
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function doesNotDeleteDataOnOtherPid(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/Database/OtherExistingTeas.csv');
diff --git a/Tests/Functional/Controller/TeaControllerTest.php b/Tests/Functional/Controller/TeaControllerTest.php
index f55df84..27715da 100644
--- a/Tests/Functional/Controller/TeaControllerTest.php
+++ b/Tests/Functional/Controller/TeaControllerTest.php
@@ -4,12 +4,13 @@ declare(strict_types=1);
 
 namespace TTN\Tea\Tests\Functional\Controller;
 
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use TTN\Tea\Controller\TeaController;
 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
-/**
- * @covers \TTN\Tea\Controller\TeaController
- */
+#[CoversClass(TeaController::class)]
 final class TeaControllerTest extends FunctionalTestCase
 {
     protected array $testExtensionsToLoad = ['ttn/tea'];
@@ -49,9 +50,7 @@ final class TeaControllerTest extends FunctionalTestCase
         $this->importCSVDataSet(__DIR__ . '/Fixtures/Database/Teas.csv');
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function indexActionRendersAllAvailableTeas(): void
     {
         $request = (new InternalRequest())->withPageId(1);
@@ -62,9 +61,7 @@ final class TeaControllerTest extends FunctionalTestCase
         self::assertStringContainsString('Oolong', $html);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function showActionRendersTheGivenTeas(): void
     {
         $request = (new InternalRequest())->withPageId(3)->withQueryParameters(['tx_tea_teashow[tea]' => 1]);
diff --git a/Tests/Functional/Domain/Repository/TeaRepositoryTest.php b/Tests/Functional/Domain/Repository/TeaRepositoryTest.php
index 08f6558..ce214a4 100644
--- a/Tests/Functional/Domain/Repository/TeaRepositoryTest.php
+++ b/Tests/Functional/Domain/Repository/TeaRepositoryTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace TTN\Tea\Tests\Functional\Domain\Repository;
 
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
 use TTN\Tea\Domain\Model\Tea;
 use TTN\Tea\Domain\Repository\TeaRepository;
 use TYPO3\CMS\Extbase\Domain\Model\FileReference;
@@ -11,10 +13,8 @@ use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
 use TYPO3\CMS\Extbase\Persistence\Repository;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
-/**
- * @covers \TTN\Tea\Domain\Repository\TeaRepository
- * @covers \TTN\Tea\Domain\Model\Tea
- */
+#[CoversClass(TeaRepository::class)]
+#[CoversClass(Tea::class)]
 final class TeaRepositoryTest extends FunctionalTestCase
 {
     protected array $testExtensionsToLoad = ['ttn/tea'];
@@ -32,17 +32,13 @@ final class TeaRepositoryTest extends FunctionalTestCase
         $this->subject = $this->get(TeaRepository::class);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function isRepository(): void
     {
         self::assertInstanceOf(Repository::class, $this->subject);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findAllForNoRecordsReturnsEmptyContainer(): void
     {
         $result = $this->subject->findAll();
@@ -50,9 +46,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertCount(0, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findAllSortsByTitleInAscendingOrder(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TwoUnsortedTeas.csv');
@@ -63,9 +57,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertSame(2, $result->current()->getUid());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByUidForInexistentRecordReturnsNull(): void
     {
         $model = $this->subject->findByUid(1);
@@ -73,9 +65,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertNull($model);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByUidForExistingRecordReturnsModel(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TeaWithAllScalarData.csv');
@@ -85,9 +75,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertInstanceOf(Tea::class, $model);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByUidForExistingRecordMapsAllScalarData(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TeaWithAllScalarData.csv');
@@ -100,9 +88,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertSame(2, $model->getOwnerUid());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function fillsImageRelation(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TeaWithImage.csv');
@@ -115,9 +101,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertSame(1, $image->getUid());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function addAndPersistAllCreatesNewRecord(): void
     {
         $title = 'Godesberger Burgtee';
@@ -130,9 +114,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         $this->assertCSVDataSet(__DIR__ . '/Fixtures/PersistedTea.csv');
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByOwnerUidFindsTeaWithTheGivenOwnerUid(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TeaWithOwner.csv');
@@ -142,9 +124,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertCount(1, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByOwnerUidFindsTeaWithTheGivenOwnerUidOnPage(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TeaWithOwnerOnPage.csv');
@@ -154,9 +134,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertCount(1, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByOwnerUidFindsIgnoresTeaWithNonMatchingOwnerUid(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TeaWithOwner.csv');
@@ -166,9 +144,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertCount(0, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByOwnerUidFindsIgnoresTeaWithZeroOwnerUid(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TeaWithoutOwner.csv');
@@ -178,9 +154,7 @@ final class TeaRepositoryTest extends FunctionalTestCase
         self::assertCount(0, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function findByOwnerUidSortsByTitleInAscendingOrder(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/TwoTeasWithOwner.csv');
diff --git a/Tests/Unit/Controller/FrontEndEditorControllerTest.php b/Tests/Unit/Controller/FrontEndEditorControllerTest.php
index a90712b..9b6e2d6 100644
--- a/Tests/Unit/Controller/FrontEndEditorControllerTest.php
+++ b/Tests/Unit/Controller/FrontEndEditorControllerTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace TTN\Tea\Tests\Unit\Controller;
 
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
 use PHPUnit\Framework\MockObject\MockObject;
 use TTN\Tea\Controller\FrontEndEditorController;
 use TTN\Tea\Domain\Model\Tea;
@@ -22,9 +24,8 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 /**
  * Note: Unit tests for controllers are not considered best practice anymore. Instead, functional tests should be used.
  * We're currently in the process of migrating all controller tests to functional tests.
- *
- * @covers \TTN\Tea\Controller\FrontEndEditorController
  */
+#[CoversClass(FrontEndEditorController::class)]
 final class FrontEndEditorControllerTest extends UnitTestCase
 {
     /**
@@ -62,7 +63,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->viewMock = $this->createMock(TemplateView::class);
         $this->subject->_set('view', $this->viewMock);
 
-        $responseStub = $this->createStub(HtmlResponse::class);
+        $responseStub = self::createStub(HtmlResponse::class);
         $this->subject->method('htmlResponse')->willReturn($responseStub);
     }
 
@@ -84,17 +85,13 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->context->setAspect('frontend.user', $userAspectMock);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function isActionController(): void
     {
         self::assertInstanceOf(ActionController::class, $this->subject);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function indexActionForNoLoggedInUserAssignsNothingToView(): void
     {
         $this->setUidOfLoggedInUser(0);
@@ -104,24 +101,20 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->indexAction();
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function indexActionForLoggedInUserAssignsTeasOwnedByTheLoggedInUserToView(): void
     {
         $userUid = 5;
         $this->setUidOfLoggedInUser($userUid);
 
-        $teas = $this->createStub(QueryResultInterface::class);
+        $teas = self::createStub(QueryResultInterface::class);
         $this->teaRepositoryMock->method('findByOwnerUid')->with($userUid)->willReturn($teas);
         $this->viewMock->expects(self::once())->method('assign')->with('teas', $teas);
 
         $this->subject->indexAction();
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function indexActionReturnsHtmlResponse(): void
     {
         $result = $this->subject->indexAction();
@@ -129,9 +122,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         self::assertInstanceOf(HtmlResponse::class, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function editActionWithOwnTeaAssignsProvidedTeaToView(): void
     {
         $userUid = 5;
@@ -144,9 +135,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->editAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function editActionWithTeaFromOtherUserThrowsException(): void
     {
         $this->setUidOfLoggedInUser(1);
@@ -160,9 +149,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->editAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function editActionWithTeaWithoutOwnerThrowsException(): void
     {
         $this->setUidOfLoggedInUser(1);
@@ -176,9 +163,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->editAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function editActionForOwnTeaReturnsHtmlResponse(): void
     {
         $userUid = 5;
@@ -191,9 +176,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         self::assertInstanceOf(HtmlResponse::class, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function updateActionWithOwnTeaPersistsProvidedTea(): void
     {
         $userUid = 5;
@@ -209,20 +192,18 @@ final class FrontEndEditorControllerTest extends UnitTestCase
 
     private function mockRedirect(string $actionName): void
     {
-        $redirectResponse = $this->createStub(RedirectResponse::class);
+        $redirectResponse = self::createStub(RedirectResponse::class);
         $this->subject->expects(self::once())->method('redirect')->with($actionName)
             ->willReturn($redirectResponse);
     }
 
     private function stubRedirect(string $actionName): void
     {
-        $redirectResponse = $this->createStub(RedirectResponse::class);
+        $redirectResponse = self::createStub(RedirectResponse::class);
         $this->subject->method('redirect')->willReturn($redirectResponse);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function updateActionWithOwnTeaRedirectsToIndexAction(): void
     {
         $userUid = 5;
@@ -235,9 +216,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->updateAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function updateActionWithTeaFromOtherUserThrowsException(): void
     {
         $this->setUidOfLoggedInUser(1);
@@ -251,9 +230,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->updateAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function updateActionWithTeaWithoutOwnerThrowsException(): void
     {
         $this->setUidOfLoggedInUser(1);
@@ -267,9 +244,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->updateAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function newActionWithTeaAssignsProvidedTeaToView(): void
     {
         $tea = new Tea();
@@ -279,9 +254,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->newAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function newActionWithNullTeaAssignsProvidedNewTeaToView(): void
     {
         $tea = new Tea();
@@ -292,9 +265,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->newAction(null);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function newActionWithoutTeaAssignsProvidedNewTeaToView(): void
     {
         $tea = new Tea();
@@ -305,9 +276,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->newAction();
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function newActionReturnsHtmlResponse(): void
     {
         $result = $this->subject->newAction();
@@ -315,9 +284,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         self::assertInstanceOf(HtmlResponse::class, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function createActionSetsLoggedInUserAsOwnerOfProvidedTea(): void
     {
         $userUid = 5;
@@ -330,9 +297,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         self::assertSame($userUid, $tea->getOwnerUid());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function createActionPersistsProvidedTea(): void
     {
         $tea = new Tea();
@@ -343,9 +308,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->createAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function createActionRedirectsToIndexAction(): void
     {
         $tea = new Tea();
@@ -355,9 +318,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->updateAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function deleteActionWithOwnTeaRemovesProvidedTea(): void
     {
         $userUid = 5;
@@ -371,9 +332,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->deleteAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function deleteActionWithOwnTeaRedirectsToIndexAction(): void
     {
         $userUid = 5;
@@ -386,9 +345,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->deleteAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function deleteActionWithTeaFromOtherUserThrowsException(): void
     {
         $this->setUidOfLoggedInUser(1);
@@ -402,9 +359,7 @@ final class FrontEndEditorControllerTest extends UnitTestCase
         $this->subject->deleteAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function deleteActionWithTeaWithoutOwnerThrowsException(): void
     {
         $this->setUidOfLoggedInUser(1);
diff --git a/Tests/Unit/Controller/TeaControllerTest.php b/Tests/Unit/Controller/TeaControllerTest.php
index 568f1f4..751ee31 100644
--- a/Tests/Unit/Controller/TeaControllerTest.php
+++ b/Tests/Unit/Controller/TeaControllerTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace TTN\Tea\Tests\Unit\Controller;
 
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
 use PHPUnit\Framework\MockObject\MockObject;
 use TTN\Tea\Controller\TeaController;
 use TTN\Tea\Domain\Model\Tea;
@@ -18,9 +20,8 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 /**
  * Note: Unit tests for controllers are not considered best practice anymore. Instead, functional tests should be used.
  * We're currently in the process of migrating all controller tests to functional tests.
- *
- * @covers \TTN\Tea\Controller\TeaController
  */
+#[CoversClass(TeaController::class)]
 final class TeaControllerTest extends UnitTestCase
 {
     /**
@@ -50,33 +51,27 @@ final class TeaControllerTest extends UnitTestCase
         $this->viewMock = $this->createMock(TemplateView::class);
         $this->subject->_set('view', $this->viewMock);
 
-        $responseStub = $this->createStub(HtmlResponse::class);
+        $responseStub = self::createStub(HtmlResponse::class);
         $this->subject->method('htmlResponse')->willReturn($responseStub);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function isActionController(): void
     {
         self::assertInstanceOf(ActionController::class, $this->subject);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function indexActionAssignsAllTeaAsTeasToView(): void
     {
-        $teas = $this->createStub(QueryResultInterface::class);
+        $teas = self::createStub(QueryResultInterface::class);
         $this->teaRepositoryMock->method('findAll')->willReturn($teas);
         $this->viewMock->expects(self::once())->method('assign')->with('teas', $teas);
 
         $this->subject->indexAction();
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function indexActionReturnsHtmlResponse(): void
     {
         $result = $this->subject->indexAction();
@@ -84,9 +79,7 @@ final class TeaControllerTest extends UnitTestCase
         self::assertInstanceOf(HtmlResponse::class, $result);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function showActionAssignsPassedTeaAsTeaToView(): void
     {
         $tea = new Tea();
@@ -95,9 +88,7 @@ final class TeaControllerTest extends UnitTestCase
         $this->subject->showAction($tea);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function showActionAssignsReturnsHtmlResponse(): void
     {
         $result = $this->subject->showAction(new Tea());
diff --git a/Tests/Unit/Domain/Model/TeaTest.php b/Tests/Unit/Domain/Model/TeaTest.php
index 6d8c5bc..954411d 100644
--- a/Tests/Unit/Domain/Model/TeaTest.php
+++ b/Tests/Unit/Domain/Model/TeaTest.php
@@ -4,14 +4,14 @@ declare(strict_types=1);
 
 namespace TTN\Tea\Tests\Unit\Domain\Model;
 
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
 use TTN\Tea\Domain\Model\Tea;
 use TYPO3\CMS\Extbase\Domain\Model\FileReference;
 use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
-/**
- * @covers \TTN\Tea\Domain\Model\Tea
- */
+#[CoversClass(Tea::class)]
 final class TeaTest extends UnitTestCase
 {
     private Tea $subject;
@@ -23,25 +23,19 @@ final class TeaTest extends UnitTestCase
         $this->subject = new Tea();
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function isAbstractEntity(): void
     {
         self::assertInstanceOf(AbstractEntity::class, $this->subject);
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function getTitleInitiallyReturnsEmptyString(): void
     {
         self::assertSame('', $this->subject->getTitle());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function setTitleSetsTitle(): void
     {
         $value = 'Earl Grey';
@@ -50,17 +44,13 @@ final class TeaTest extends UnitTestCase
         self::assertSame($value, $this->subject->getTitle());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function getDescriptionInitiallyReturnsEmptyString(): void
     {
         self::assertSame('', $this->subject->getDescription());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function setDescriptionSetsDescription(): void
     {
         $value = 'Very refreshing and amoratic.';
@@ -69,17 +59,13 @@ final class TeaTest extends UnitTestCase
         self::assertSame($value, $this->subject->getDescription());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function getImageInitiallyReturnsNull(): void
     {
         self::assertNull($this->subject->getImage());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function setImageSetsImage(): void
     {
         $model = new FileReference();
@@ -88,17 +74,13 @@ final class TeaTest extends UnitTestCase
         self::assertSame($model, $this->subject->getImage());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function getOwnerUidInitiallyReturnsZero(): void
     {
         self::assertSame(0, $this->subject->getOwnerUid());
     }
 
-    /**
-     * @test
-     */
+    #[Test]
     public function setOwnerUidSetsOwnerUid(): void
     {
         $value = 123456;
diff --git a/composer.json b/composer.json
index 36aa278..ae7e34d 100644
--- a/composer.json
+++ b/composer.json
@@ -47,7 +47,7 @@
 		"typo3/cms-frontend": "^12.4.26"
 	},
 	"require-dev": {
-		"brianium/paratest": "6.11.1",
+		"brianium/paratest": "7.3.1 || 7.4.8",
 		"ergebnis/composer-normalize": "2.45.0",
 		"friendsofphp/php-cs-fixer": "3.68.5",
 		"helmich/typo3-typoscript-lint": "^3.3.0",
@@ -58,7 +58,7 @@
 		"phpstan/phpstan": "1.12.14",
 		"phpstan/phpstan-phpunit": "1.4.2",
 		"phpstan/phpstan-strict-rules": "1.6.1",
-		"phpunit/phpunit": "9.6.22",
+		"phpunit/phpunit": "10.5.31 || 10.5.44",
 		"saschaegerer/phpstan-typo3": "1.10.2",
 		"seld/jsonlint": "1.11.0",
 		"spaze/phpstan-disallowed-calls": "4.2.1",
diff --git a/phive.xml b/phive.xml
index 1572cab..93d62fd 100644
--- a/phive.xml
+++ b/phive.xml
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phive xmlns="https://phar.io/phive">
-  <phar name="phpcov" version="^8.2.1" installed="8.2.1" location="./tools/phpcov" copy="false"/>
+  <phar name="phpcov" version="^9.0.2" installed="9.0.2" location="./tools/phpcov" copy="false"/>
 </phive>
diff --git a/rector.php b/rector.php
index e8f7fdd..3ab258d 100644
--- a/rector.php
+++ b/rector.php
@@ -64,13 +64,7 @@ return RectorConfig::configure()
         // PHPUnit sets
 
         // PHPUnitSetList::PHPUNIT80_DMS,
-        // PHPUnitSetList::PHPUNIT_40,
-        // PHPUnitSetList::PHPUNIT_50,
-        // PHPUnitSetList::PHPUNIT_60,
-        // PHPUnitSetList::PHPUNIT_70,
-        // PHPUnitSetList::PHPUNIT_80,
-        // PHPUnitSetList::PHPUNIT_90,
-        // PHPUnitSetList::PHPUNIT_100,
+        PHPUnitSetList::PHPUNIT_100,
         // PHPUnitSetList::PHPUNIT_CODE_QUALITY,
 
         // TYPO3 Sets