diff --git a/composer.json b/composer.json index 0bbb06db2..7b74a17d6 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,7 @@ "phpstan/phpstan": "^1.12", "phpstan/phpstan-phpunit": "^1.4", "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.0", "rector/rector": "^0.18.2", "sylius-labs/coding-standard": "^4.4", "sylius/grid-bundle": "^1.13", @@ -77,16 +77,17 @@ "symfony/dependency-injection": "^6.4 || ^7.1", "symfony/dotenv": "^6.4 || ^7.1", "symfony/http-kernel": "^6.4 || ^7.1", + "symfony/messenger": "^6.4 || ^7.1", + "symfony/security-bundle": "^6.4 || ^7.1", + "symfony/serializer": "^6.4 || ^7.1", "symfony/stopwatch": "^6.4 || ^7.1", "symfony/uid": "^6.4 || ^7.1", "symfony/workflow": "^6.4 || ^7.1", - "symfony/messenger": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", - "symfony/security-bundle": "^6.4 || ^7.1", "twig/twig": "^3.14", "vimeo/psalm": "^5.26", "willdurand/hateoas-bundle": "^2.5", - "winzou/state-machine-bundle": "^0.6.2" + "winzou/state-machine-bundle": "^0.6.2", + "zenstruck/foundry": "^2.3" }, "conflict": { "doctrine/orm": "<2.18", diff --git a/tests/Application/config/bundles.php b/tests/Application/config/bundles.php index 1eb88eee0..4e0edaf5b 100644 --- a/tests/Application/config/bundles.php +++ b/tests/Application/config/bundles.php @@ -39,4 +39,5 @@ NelmioAliceBundle::class => ['all' => true], winzouStateMachineBundle::class => ['all' => true, 'test_without_state_machine' => false], SyliusGridBundle::class => ['all' => true, 'test_without_twig' => false], + Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true], ]; diff --git a/tests/Application/src/Foundry/Factory/BookFactory.php b/tests/Application/src/Foundry/Factory/BookFactory.php new file mode 100644 index 000000000..cdaa61633 --- /dev/null +++ b/tests/Application/src/Foundry/Factory/BookFactory.php @@ -0,0 +1,52 @@ + + */ +final class BookFactory extends PersistentProxyObjectFactory +{ + public static function class(): string + { + return Book::class; + } + + public function withTranslations(array $translations): self + { + return $this->with(['translations' => $translations]); + } + + public function withTitle(string $title): self + { + return $this->with(['title' => $title]); + } + + public function withAuthor(string $author): self + { + return $this->with(['author' => $author]); + } + + protected function defaults(): array + { + return [ + 'fallbackLocale' => 'en_US', + 'currentLocale' => 'en_US', + 'author' => self::faker()->firstName() . ' ' . self::faker()->lastName(), + ]; + } +} diff --git a/tests/Application/src/Foundry/Factory/BookTranslationFactory.php b/tests/Application/src/Foundry/Factory/BookTranslationFactory.php new file mode 100644 index 000000000..72efd16fc --- /dev/null +++ b/tests/Application/src/Foundry/Factory/BookTranslationFactory.php @@ -0,0 +1,46 @@ + + */ +final class BookTranslationFactory extends PersistentProxyObjectFactory +{ + public static function class(): string + { + return BookTranslation::class; + } + + public function withLocale(string $locale): self + { + return $this->with(['locale' => $locale]); + } + + public function withTitle(string $title): self + { + return $this->with(['title' => $title]); + } + + protected function defaults(): array + { + return [ + 'locale' => self::faker()->locale(), + 'title' => ucfirst(self::faker()->words(2, true)), + ]; + } +} diff --git a/tests/Application/src/Foundry/Story/DefaultBooksStory.php b/tests/Application/src/Foundry/Story/DefaultBooksStory.php new file mode 100644 index 000000000..05b5315e6 --- /dev/null +++ b/tests/Application/src/Foundry/Story/DefaultBooksStory.php @@ -0,0 +1,43 @@ +withTranslations([ + BookTranslationFactory::new() + ->withLocale('en_US') + ->withTitle('Lord of The Rings'), + BookTranslationFactory::new() + ->withLocale('pl_PL') + ->withTitle('Władca Pierścieni'), + ]) + ->withAuthor('J.R.R. Tolkien') + ->create() + ; + + BookFactory::new() + ->withTitle('Game of Thrones') + ->withAuthor('George R. R. Martin') + ->create() + ; + } +} diff --git a/tests/Application/src/Foundry/Story/MoreBooksStory.php b/tests/Application/src/Foundry/Story/MoreBooksStory.php new file mode 100644 index 000000000..07599bc99 --- /dev/null +++ b/tests/Application/src/Foundry/Story/MoreBooksStory.php @@ -0,0 +1,33 @@ +withTitle('Book ' . $number) + ->create() + ; + } + }); + } +} diff --git a/tests/Application/src/Tests/Controller/BookApiTest.php b/tests/Application/src/Tests/Controller/BookApiTest.php index cfec84916..704ca4964 100644 --- a/tests/Application/src/Tests/Controller/BookApiTest.php +++ b/tests/Application/src/Tests/Controller/BookApiTest.php @@ -14,14 +14,22 @@ namespace App\Tests\Controller; use ApiTestCase\JsonApiTestCase; +use App\Foundry\Factory\BookFactory; +use App\Foundry\Factory\BookTranslationFactory; +use App\Foundry\Story\DefaultBooksStory; +use App\Foundry\Story\MoreBooksStory; +use PHPUnit\Framework\Attributes\Test; use Symfony\Component\HttpFoundation\Response; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; class BookApiTest extends JsonApiTestCase { - /** - * @test - */ - public function it_allows_creating_a_book() + use Factories; + use ResetDatabase; + + #[Test] + public function it_allows_creating_a_book(): void { $this->markAsSkippedIfNecessary(); @@ -42,12 +50,10 @@ public function it_allows_creating_a_book() $this->assertResponse($response, 'books/create_response', Response::HTTP_CREATED); } - /** - * @test - */ - public function it_allows_updating_a_book() + #[Test] + public function it_allows_updating_a_book(): void { - $objects = $this->loadFixturesFromFile('books.yml'); + $book = BookFactory::createOne(); $data = <<client->request('PUT', '/books/' . $objects['book1']->getId(), [], [], ['CONTENT_TYPE' => 'application/json'], $data); + $this->client->request('PUT', '/books/' . $book->getId(), [], [], ['CONTENT_TYPE' => 'application/json'], $data); $response = $this->client->getResponse(); $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); } - /** - * @test - */ - public function it_allows_updating_partial_information_about_a_book() + #[Test] + public function it_allows_updating_partial_information_about_a_book(): void { - $objects = $this->loadFixturesFromFile('books.yml'); + $book = BookFactory::createOne(); $data = <<client->request('PATCH', '/books/' . $objects['book1']->getId(), [], [], ['CONTENT_TYPE' => 'application/json'], $data); + $this->client->request('PATCH', '/books/' . $book->getId(), [], [], ['CONTENT_TYPE' => 'application/json'], $data); $response = $this->client->getResponse(); $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); } - /** - * @test - */ - public function it_allows_removing_a_book() + #[Test] + public function it_allows_removing_a_book(): void { - $objects = $this->loadFixturesFromFile('books.yml'); + $book = BookFactory::createOne(); - $this->client->request('DELETE', '/books/' . $objects['book1']->getId()); + $this->client->request('DELETE', '/books/' . $book->getId()); $response = $this->client->getResponse(); $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); } - /** - * @test - */ - public function it_allows_showing_a_book() + #[Test] + public function it_allows_showing_a_book(): void { $this->markAsSkippedIfNecessary(); - $objects = $this->loadFixturesFromFile('books.yml'); - - $this->client->request('GET', '/books/' . $objects['book1']->getId()); + $book = BookFactory::new() + ->withTranslations([ + BookTranslationFactory::new() + ->withLocale('en_US') + ->withTitle('Lord of The Rings'), + BookTranslationFactory::new() + ->withLocale('pl_PL') + ->withTitle('Władca Pierścieni'), + ]) + ->withAuthor('J.R.R. Tolkien') + ->create() + ; + + $this->client->request('GET', '/books/' . $book->getId()); $response = $this->client->getResponse(); $this->assertResponse($response, 'books/show_response'); } - /** - * @test - */ - public function it_allows_indexing_books() + #[Test] + public function it_allows_indexing_books(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('books.yml'); + DefaultBooksStory::load(); $this->client->request('GET', '/books/'); $response = $this->client->getResponse(); $this->assertResponse($response, 'books/index_response'); } - /** - * @test - */ - public function it_allows_paginating_the_index_of_books() + #[Test] + public function it_allows_paginating_the_index_of_books(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('more_books.yml'); + MoreBooksStory::load(); $this->client->request('GET', '/books/', ['page' => 2]); $response = $this->client->getResponse(); $this->assertResponse($response, 'books/paginated_index_response'); } - /** - * @test - */ - public function it_does_not_allow_showing_resource_if_it_not_exists() + #[Test] + public function it_does_not_allow_showing_resource_if_it_not_exists(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('books.yml'); + DefaultBooksStory::load(); $this->client->request('GET', '/books/3'); $response = $this->client->getResponse(); $this->assertResponseCode($response, Response::HTTP_NOT_FOUND); } - /** - * @test - */ - public function it_does_not_apply_sorting_for_un_existing_field() + #[Test] + public function it_does_not_apply_sorting_for_un_existing_field(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('more_books.yml'); + MoreBooksStory::load(); $this->client->request('GET', '/sortable-books/', ['sorting' => ['name' => 'DESC']]); $response = $this->client->getResponse(); @@ -171,14 +174,12 @@ public function it_does_not_apply_sorting_for_un_existing_field() $this->assertResponseCode($response, Response::HTTP_OK); } - /** - * @test - */ - public function it_does_not_apply_filtering_for_un_existing_field() + #[Test] + public function it_does_not_apply_filtering_for_un_existing_field(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('more_books.yml'); + MoreBooksStory::load(); $this->client->request('GET', '/filterable-books/', ['criteria' => ['name' => 'John']]); $response = $this->client->getResponse(); @@ -186,14 +187,12 @@ public function it_does_not_apply_filtering_for_un_existing_field() $this->assertResponseCode($response, Response::HTTP_OK); } - /** - * @test - */ - public function it_applies_sorting_for_existing_field() + #[Test] + public function it_applies_sorting_for_existing_field(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('more_books.yml'); + MoreBooksStory::load(); $this->client->request('GET', '/sortable-books/', ['sorting' => ['id' => 'DESC']]); $response = $this->client->getResponse(); @@ -201,14 +200,12 @@ public function it_applies_sorting_for_existing_field() $this->assertResponseCode($response, Response::HTTP_OK); } - /** - * @test - */ - public function it_applies_filtering_for_existing_field() + #[Test] + public function it_applies_filtering_for_existing_field(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('more_books.yml'); + MoreBooksStory::load(); $this->client->request('GET', '/filterable-books/', ['criteria' => ['author' => 'J.R.R. Tolkien']]); $response = $this->client->getResponse(); @@ -216,10 +213,8 @@ public function it_applies_filtering_for_existing_field() $this->assertResponseCode($response, Response::HTTP_OK); } - /** - * @test - */ - public function it_allows_creating_a_book_via_custom_factory() + #[Test] + public function it_allows_creating_a_book_via_custom_factory(): void { $this->markAsSkippedIfNecessary(); @@ -240,28 +235,24 @@ public function it_allows_creating_a_book_via_custom_factory() $this->assertResponse($response, 'books/create_response', Response::HTTP_CREATED); } - /** - * @test - */ + #[Test] public function it_allows_indexing_books_via_custom_repository(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('books.yml'); + DefaultBooksStory::load(); $this->client->request('GET', '/find-custom-books'); $response = $this->client->getResponse(); $this->assertResponse($response, 'books/index_response'); } - /** - * @test - */ - public function it_allows_showing_a_book_via_custom_repository() + #[Test] + public function it_allows_showing_a_book_via_custom_repository(): void { $this->markAsSkippedIfNecessary(); - $this->loadFixturesFromFile('books.yml'); + DefaultBooksStory::load(); $this->client->request('GET', '/find-custom-book'); $response = $this->client->getResponse();