All jobs

Write PHPUnit Tests for a Symfony App

Yes. An agent writes WebTestCase tests for a Symfony admin area, covering the security voter, form validation errors and CSRF, runs PHPUnit and opens a pull request.

Run this on RushIn a Project that has your repository.

What the agent does

The agent reads the test setup before it writes a test, because a Symfony functional suite depends on how the kernel, the database and authentication are wired for the test environment. In symfony/demo, composer.json requires PHP 8.4 or later, phpunit/phpunit 11.5 and dama/doctrine-test-bundle, with symfony/browser-kit and symfony/css-selector as the crawler. phpunit.dist.xml boots tests/bootstrap.php, turns failOnDeprecation, failOnNotice and failOnWarning on, and registers the DAMA extension that wraps every test in a transaction. .env.test points DATABASE_URL at data/database_test.sqlite, a file committed to the repository, and config/packages/security.yaml switches the main firewall to HTTP basic under when@test. .github/workflows/tests.yaml runs vendor/bin/phpunit on PHP 8.4 and 8.5 on Ubuntu and on 8.4 on macOS and Windows.

Then it maps what the admin area does against what tests/Controller/Admin/BlogControllerTest.php already covers. src/Controller/Admin/BlogController.php sits under /admin/post behind #[IsGranted(User::ROLE_ADMIN)], with new, show, edit and delete actions; show calls denyAccessUnlessGranted(PostVoter::SHOW, $post), edit and delete carry #[IsGranted('edit')] and #[IsGranted('delete')] on the post, and delete checks a CSRF token named delete from the request payload before removing anything. src/Security/PostVoter.php grants all three only when the logged-in user is the post's author. The existing test logs in as jane_admin, checks that john_user gets 403 on four URLs, and covers listing, creating, a duplicated title, showing, editing and deleting post 1, which src/DataFixtures/AppFixtures.php assigns to jane_admin.

The gaps are the paths that test fixture happens to avoid. tom_admin has ROLE_ADMIN too but is not the author of post 1, so GET /en/admin/post/1, GET /en/admin/post/1/edit and POST /en/admin/post/1/delete as tom_admin must be 403 from the voter, not from the role check. src/Form/PostType.php adds a saveAndCreateNew submit button that redirects to admin_post_new instead of the index. src/Entity/Post.php declares NotBlank on the summary with message post.blank_summary, Length(max: 255) on it, Length(min: 10) on the content with post.too_short_content, Count(max: 4) on tags with post.too_many_tags, and UniqueEntity on the slug reported on the title field. A delete request with a wrong token must redirect to the index and leave the post in place.

The agent writes those cases into the existing class, following its conventions: loginUser with a user from UserRepository, submitForm with post[...] field names, assertResponseRedirects with Response::HTTP_SEE_OTHER, and assertSelectorTextContains('form .invalid-feedback', ...) with the translated message from translations/validators+intl-icu.en.xlf. For the voter cases it clears the cookie jar and logs in as tom_admin; for the CSRF case it posts to the delete URL with a made-up token and then reads the post back through PostRepository. It runs vendor/bin/phpunit, which must stay free of deprecations because of the failOn flags, and php-cs-fixer on the changed file, then opens a pull request with the suite output.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Write PHPUnit functional tests for this Symfony app's admin area, extending tests/Controller/Admin/BlogControllerTest.php in its existing style. Cover: an admin who is not the author getting 403 on show, edit and delete through PostVoter; the saveAndCreateNew button redirecting to the new-post form; validation errors for a blank summary, a summary over 255 characters, content under 10 characters and more than 4 tags, asserting the translated message in form .invalid-feedback; and a delete with an invalid CSRF token that redirects and leaves the post in place. Use loginUser with fixture users, keep production code unchanged, run vendor/bin/phpunit and php-cs-fixer on the changed file, and open a pull request with the suite output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is a pull request that changes tests/Controller/Admin/BlogControllerTest.php and nothing under src/. The new methods sit beside the existing ones, use the same loginUser and submitForm calls, and name the fixture users and the post id they rely on. Its description lists each behavior now covered, quotes the vendor/bin/phpunit summary with no deprecation or warning failures, and shows the php-cs-fixer result.

Before merging, a reviewer checks that the voter cases log in as an admin who is not the author rather than as a regular user, since the role check would mask the voter; that the validation assertions match the English messages in translations/validators+intl-icu.en.xlf; and that the CSRF case asserts the post still exists after the redirect. A test that needed a change in src/ to pass is a finding to discuss, not a test to merge.

Run it on every night

Create an automation in the Project with this prompt and a schedule trigger (cron 0 2 * * * in your timezone). The prompt names one gap, so once these cases are merged, change it to name the next controller or action without tests; each night's run writes the cases its prompt names and opens one pull request by morning, or opens none when they are already covered.

To have the link reach your server, start the nightly run from your own scheduler instead, with POST /o/{org}/sessions and an API key that has a webhook URL: Rush then POSTs run.completed with the session_url when the turn ends, and a handler can post the pull request link where the team reads first thing. One pull request per run keeps the review small.

The use case this job follows, with its automation and trigger: Write the missing tests.

Why run it on Rush?

A Symfony functional suite is quick to run and slow to extend by hand: each new case needs the right fixture user, the right form field names, the exact translated message and a database the DAMA bundle can roll back. The agent reads all of that from the repository in its own sandbox, with composer install done and the committed SQLite test database in place, and writes tests that run green the first time or are fixed until they do.

The pull request shows the suite output and the trajectory keeps every run, including the failing attempts, so a reviewer sees which assertion was adjusted and why. The same request runs against every Symfony app you maintain in parallel, each in its own sandbox.

Frequently asked questions

How does it authenticate in tests?

With loginUser on the test client, as the existing test does, using users loaded from UserRepository: jane_admin and tom_admin with ROLE_ADMIN and john_user with ROLE_USER, all from AppFixtures. The when@test block in security.yaml also enables HTTP basic, but the agent follows the file it is extending.

Will the new tests leave data behind?

No. phpunit.dist.xml registers the DAMA Doctrine test bundle extension, which starts a transaction before each test and rolls it back after, so a created or deleted post is gone before the next test runs. The agent does not add its own cleanup.

Does it change the controller or the voter?

No. The prompt keeps production code unchanged. If a test it writes cannot pass without a change, for example a status code that differs from the one the controller documents, the pull request reports that as a finding and leaves the test out.

What about the deprecation flags?

failOnDeprecation, failOnNotice and failOnWarning are on, so a test that triggers a deprecated call fails the suite. The agent runs the full suite rather than the one file so a new helper that trips a deprecation is caught before the pull request opens.