diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 36b011a469c9428e7a48ed6faf574ca4d9e4f0e5..bee7aa11543c092ac46d22be8d820b2006a8d2a1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-# cspell:ignore codequality Micheh micheh webide updatedb stylelintrc unshallow
+# cspell:ignore codequality Micheh micheh webide testdox updatedb stylelintrc
 
 ################
 # Drupal GitLabCI template.
@@ -103,6 +103,25 @@ default:
   rules:
     - if: $PERFORMANCE_TEST != "1"
 
+.default-phpunit-job-settings: &default-phpunit-job-settings
+  stage: 🪄 Lint
+  before_script:
+    - composer validate
+    - composer install --optimize-autoloader
+    - mkdir -p ./sites/simpletest ./sites/default/files ./build/logs/junit /var/www/.composer
+    - chown -R www-data:www-data ./sites ./build/logs/junit ./vendor /var/www/
+  script:
+    - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --verbose --non-html --all
+  after_script:
+    - sed -i "s#$CI_PROJECT_DIR/##" ./sites/default/files/simpletest/phpunit-*.xml || true
+  artifacts:
+    when: always
+    expire_in: 6 mos
+    reports:
+      junit: ./sites/default/files/simpletest/phpunit-*.xml
+    paths:
+      - ./sites/default/files/simpletest/phpunit-*.xml
+
 .prepare-lint-directory: &prepare-lint-directory
   # PHPStan and yarn linting use absolute paths to determine cache validity. Because GitLab CI
   # working directories are not consistent, work around this by running linting in a separate,
@@ -312,40 +331,30 @@ default:
 # Lint Jobs
 ################
 
-
-'Lint cache warming':
+'📔 Spell-checking':
   <<: [ *default-job-settings-lint ]
   stage: 🪄 Lint
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ROOT_NAMESPACE == "project"
-    - if: $CI_PIPELINE_SOURCE == "schedule" && $CI_PROJECT_ROOT_NAMESPACE == "project" && $DAILY_TEST == "1"
-    - when: manual
-      allow_failure: true
   variables:
-    KUBERNETES_CPU_REQUEST: "4"
+    KUBERNETES_CPU_REQUEST: "2"
   script:
     - *prepare-lint-directory
-    - *phpstan-cache
     - *cspell-cache
-    - *eslint-cache
-    - *stylelint-cache
-    - composer install
-    - vendor/bin/phpstan --version
-    - php vendor/bin/phpstan -vvv analyze --configuration=./core/phpstan.neon.dist
     - *core-spellcheck
-    - yarn run lint:core-js-passing --cache --cache-strategy content
-    - yarn run build:css --check
-    - yarn run lint:css --cache --cache-location .stylelintcache --cache-strategy content
-    - mv -f /build/core/phpstan-tmp $CI_PROJECT_DIR/core
-    - mv -f /build/core/.cspellcache $CI_PROJECT_DIR/core
-    - mv -f  /build/core/.eslintcache $CI_PROJECT_DIR/core
-    - mv -f  /build/core/.stylelintcache $CI_PROJECT_DIR/core
+    - mv -f /build/core/package.json $CI_PROJECT_DIR/core/package.json
+    - mv -f /build/core/yarn.lock $CI_PROJECT_DIR/core/yarn.lock
+    - mv /build/core/node_modules $CI_PROJECT_DIR/core
+  cache:
+    key:
+      files:
+        - ./core/package.json
+        - ./core/yarn.lock
+    paths:
+      - ./core/node_modules
   artifacts:
+    expire_in: 1 week
+    expose_as: 'yarn-vendor'
     paths:
-      - core/phpstan-tmp/resultCache.php
-      - core/.cspellcache
-      - core/.eslintcache
-      - core/.stylelintcache
+      - core/node_modules/
 
 '📔 Spell-checking':
   <<: [ *default-job-settings-lint ]
@@ -489,6 +498,48 @@ default:
     reports:
       codequality: gl-codequality.json
 
+'⚡️ PHPUnit Unit tests on PHP 8.3':
+  <<: [ *default-job-settings-lint, *default-phpunit-job-settings ]
+  variables:
+    TESTSUITE: PHPUnit-Unit
+    KUBERNETES_CPU_REQUEST: "8"
+    CONCURRENCY: 24
+    _TARGET_PHP: "8.3-ubuntu"
+
+'⚡️ PHPUnit Unit tests on PHP 8.4':
+  <<: [ *default-job-settings-lint, *default-phpunit-job-settings ]
+  variables:
+    TESTSUITE: PHPUnit-Unit
+    KUBERNETES_CPU_REQUEST: "8"
+    CONCURRENCY: 24
+    _TARGET_PHP: "8.4-ubuntu"
+
+'✅️ PHPStan rules tests':
+  <<: [ *default-job-settings-lint ]
+  stage: 🪄 Lint
+  variables:
+    KUBERNETES_CPU_REQUEST: "2"
+  # Run if PHPStan files have changed, or manually.
+  rules:
+    - if: $PERFORMANCE_TEST != "1"
+      changes:
+        - core/tests/PHPStan/**/*
+        - composer/Metapackage/PinnedDevDependencies/composer.json
+    - when: manual
+      allow_failure: true
+  script:
+    - docker-php-ext-enable pcov
+    - cd core/tests/PHPStan
+    - composer install
+    - vendor/bin/phpunit tests --testdox --coverage-text --colors=always --coverage-cobertura=coverage.cobertura.xml --log-junit junit.xml
+  artifacts:
+    when: always
+    reports:
+      junit: core/tests/PHPStan/junit.xml
+      coverage_report:
+        coverage_format: cobertura
+        path: core/tests/PHPStan/coverage.cobertura.xml
+
 '📔 Validatable config':
   <<: [ *default-job-settings-lint ]
   stage: 🪄 Lint
@@ -549,3 +600,36 @@ default:
          $impact = array_map(fn (float $h, float $m) => $m-$h, $head, $mr);
          exit((int) (min($impact) < 0));
       '
+
+'Lint cache warming':
+  <<: [ *default-job-settings-lint ]
+  stage: 🪄 Lint
+  rules:
+    - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ROOT_NAMESPACE == "project"
+    - if: $CI_PIPELINE_SOURCE == "schedule" && $CI_PROJECT_ROOT_NAMESPACE == "project" && $DAILY_TEST == "1"
+  variables:
+    KUBERNETES_CPU_REQUEST: "4"
+  script:
+    - *prepare-lint-directory
+    - *phpstan-cache
+    - *cspell-cache
+    - *eslint-cache
+    - *stylelint-cache
+    - composer install
+    - vendor/bin/phpstan --version
+    - php vendor/bin/phpstan -vvv analyze --configuration=./core/phpstan.neon.dist
+    - *core-spellcheck
+    - yarn run lint:core-js-passing --cache --cache-strategy content
+    - yarn run build:css --check
+    - yarn run lint:css --cache --cache-location .stylelintcache --cache-strategy content
+    - mv -f /build/core/phpstan-tmp $CI_PROJECT_DIR/core
+    - mv -f /build/core/.cspellcache $CI_PROJECT_DIR/core
+    - mv -f  /build/core/.eslintcache $CI_PROJECT_DIR/core
+    - mv -f  /build/core/.stylelintcache $CI_PROJECT_DIR/core
+  artifacts:
+    paths:
+      - core/phpstan-tmp/resultCache.php
+      - core/.cspellcache
+      - core/.eslintcache
+      - core/.stylelintcache
+
diff --git a/.gitlab-ci/pipeline.yml b/.gitlab-ci/pipeline.yml
index 3ed4a14a65c7b474757e45bbc26693fdae5b41ba..ee6f1e0e1769214f4a64d8075734239d2d7c60ab 100644
--- a/.gitlab-ci/pipeline.yml
+++ b/.gitlab-ci/pipeline.yml
@@ -1,4 +1,4 @@
-# cspell:ignore cobertura drupaltestbot drupaltestbotpw Dwebdriver logfile testdox XVFB
+# cspell:ignore drupaltestbot drupaltestbotpw Dwebdriver logfile XVFB
 
 stages:
   - 🗜️ Test
@@ -180,42 +180,6 @@ variables:
   services:
     - <<: *with-database
 
-'⚡️ PHPUnit Unit':
-  <<: [ *with-composer, *run-tests, *default-job-settings ]
-  variables:
-    TESTSUITE: PHPUnit-Unit
-    KUBERNETES_CPU_REQUEST: "1"
-    CONCURRENCY: 6
-
-'✅️ PHPStan Tests':
-  <<: [ *default-job-settings ]
-  variables:
-    KUBERNETES_CPU_REQUEST: "2"
-  # Run if PHPStan files have changed, or manually.
-  rules:
-    - if: $CI_PIPELINE_SOURCE == "parent_pipeline" && $PERFORMANCE_TEST != "1"
-      changes:
-        - core/tests/PHPStan/*
-        - composer/Metapackage/PinnedDevDependencies/composer.json
-    - when: manual
-      allow_failure: true
-  # Default job settings runs a script that expects vendor to exist.
-  before_script: []
-  script:
-    - docker-php-ext-enable pcov
-    - cd core/tests/PHPStan
-    - composer install
-    - vendor/bin/phpunit tests --testdox --coverage-text --colors=never --coverage-cobertura=coverage.cobertura.xml --log-junit junit.xml
-  # Default job settings runs a script that junit files in a specific location..
-  after_script: []
-  artifacts:
-    when: always
-    reports:
-      junit: core/tests/PHPStan/junit.xml
-      coverage_report:
-        coverage_format: cobertura
-        path: core/tests/PHPStan/coverage.cobertura.xml
-
 '🦉️️️ Nightwatch':
   <<: [ *with-composer-and-yarn, *default-job-settings ]
   variables:
diff --git a/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php b/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php
index 53a860fdc02397d8896906fa300028bfc9b597ff..397443836b85d102ab3f09cf03c3bfef913b4993 100644
--- a/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php
+++ b/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php
@@ -121,11 +121,16 @@ protected function runCommand(
     bool $colors = FALSE,
   ): void {
     global $base_url;
-    // Setup an environment variable containing the database connection so that
-    // functional tests can connect to the database.
-    $process_environment_variables = [
-      'SIMPLETEST_DB' => Database::getConnectionInfoAsUrl(),
-    ];
+    $process_environment_variables = [];
+
+    // Setup an environment variable containing the database connection if
+    // available, so that non-unit tests can connect to the database.
+    try {
+      $process_environment_variables['SIMPLETEST_DB'] = Database::getConnectionInfoAsUrl();
+    }
+    catch (\RuntimeException) {
+      // Just continue with no variable set.
+    }
 
     // Setup an environment variable containing the base URL, if it is
     // available. This allows functional tests to browse the site under test.
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 04e1f2c05258b7f2fdd7975d5bf6d93892c6399b..17a9e294295be5fbfc67a630eb90c043d125ac35 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -659,12 +659,9 @@ function simpletest_script_setup_database($new = FALSE) {
     $databases['default'] = Database::getConnectionInfo('default');
   }
 
-  // If there is no default database connection for tests, we cannot continue.
-  if (!isset($databases['default']['default'])) {
-    simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
-    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+  if (isset($databases['default']['default'])) {
+    Database::addConnectionInfo('default', 'default', $databases['default']['default']);
   }
-  Database::addConnectionInfo('default', 'default', $databases['default']['default']);
 }
 
 /**
diff --git a/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php b/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php
index 10c539db91ba0b5ddf8ba79aaf80f21e707986f9..c1ffc7df0def4c3fd41d59b33e7ad1a10483a38e 100644
--- a/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php
+++ b/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php
@@ -7,7 +7,7 @@
 use PHPUnit\Framework\TestCase;
 
 /**
- * Tests that PHPStan versions match.
+ * Tests that PHPStan version used for rules testing matches core.
  */
 class EnsurePHPStanVersionsMatchTest extends TestCase {