
最近折腾 Laravel Pest 在 CI 环境里的配置,踩了几个莫名其妙的大坑。这篇把问题说清楚,给后来者省点时间。
官方文档告诉你怎么加 MySQL 服务容器,但它不会告诉你配置完了测试还是会挂的三种隐藏原因。我全碰上了,这篇一并说。
SQLite 是 Laravel 测试的默认选项,因为它快、免配置。对于不碰数据库的单元测试,够用了。但对于功能测试,它埋了一个坑。
SQLite 的 SQL 语法和 MySQL 不一样,具体来说:
||,MySQL 用 CONCAT()strftime(),MySQL 用 DATE_FORMAT()如果你的 Eloquent 查询里用了原生 SQL(selectRaw、whereRaw、orderByRaw,报表查询这些),SQLite 会放行,MySQL 会拒绝。这问题不到生产环境根本发现不了。
老老实实用 MySQL 跑测试。多出来的配置就 20 行 YAML,不难。
在 api-ci.yml 的 test job 里加一个 services: 块:
tests: name: Tests (PHP 8.4, MySQL 8.4) runs-on: ubuntu-latest services: mysql: image: mysql:8.4 env: MYSQL_DATABASE: your_app_test MYSQL_ROOT_PASSWORD: secret ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 options: 这段是关键,它让 GitHub Actions 等 MySQL 真正启动完毕才执行后续步骤。没有这段,你的 migrate 步骤会在 MySQL 还没就绪时就去连接,然后抛出一个让人摸不着头脑的错误就挂掉了。
GitHub Actions 不认你本地的 .env 或 .env.testing,环境变量得显式声明。最干净的做法是复制 .env.example 到 .env.testing,再追加覆盖:
- name: Copy .env run: cp .env.example .env.testing - name: Set test environment variables run: | echo "APP_ENV=testing" >> .env.testing echo "APP_KEY=base64:$(openssl rand -base64 32)" >> .env.testing echo "DB_CONNECTION=mysql" >> .env.testing echo "DB_HOST=127.0.0.1" >> .env.testing echo "DB_PORT=3306" >> .env.testing echo "DB_DATABASE=your_app_test" >> .env.testing echo "DB_USERNAME=root" >> .env.testing echo "DB_PASSWORD=secret" >> .env.testing echo "QUEUE_CONNECTION=sync" >> .env.testing echo "BROADCAST_CONNECTION=log" >> .env.testing echo "CACHE_STORE=array" >> .env.testing 为什么要设 BROADCAST_CONNECTION=log? 这个坑了我很久。如果你的 .env.example 里配了 BROADCAST_CONNECTION=reverb(Laravel 11 之后的默认配置),任何触发广播事件的测试——支付完成、状态变更、通知发送——都会试图建立到 0.0.0.0:8080 的 TCP 连接。CI 环境里哪有什么 Reverb 服务器,每个这样的请求都返回 500。测试就莫名其妙地失败了,完全看不出原因。设成 log 把广播事件写到日志文件,问题就解决了。
为什么要设 QUEUE_CONNECTION=sync? 队列任务会同步立即执行,而不是推送到 Redis。没有这个配置,任何在控制器里派发任务的逻辑在测试时都会被静默跳过,不会真的执行。
- name: Run migrations run: php artisan migrate --env=testing --force env: DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: "3306" DB_DATABASE: your_app_test DB_USERNAME: root DB_PASSWORD: secret - name: Run Pest tests run: ./vendor/bin/pest env: DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: "3306" DB_DATABASE: your_app_test DB_USERNAME: root DB_PASSWORD: secret 数据库环境变量传了两遍——一次给 migrate,一次给测试。GitHub Actions 里每个步骤都在独立的 shell 环境执行,显式声明能避免很多奇怪的问题。
Git 不跟踪空目录。如果你的测试依赖 storage/framework/views/、storage/framework/sessions/ 或 bootstrap/cache/ 存在,CI 拉取代码时这些目录就是空的。
Laravel 的 package:discover(在 composer install 后运行)会启动框架,这需要这些目录存在。如果不存在,整个初始化直接崩掉,错误信息是"Please provide a valid cache path",看了完全不知道跟目录结构有关。
修复方法:在每个需要的目录里放一个 .gitignore 占位文件:
for dir in \ storage/framework/views \ storage/framework/sessions \ storage/framework/cache/data \ storage/logs \ bootstrap/cache; do mkdir -p api/$dir printf "*\n!.gitignore" > api/$dir/.gitignore
done
git add api/storage api/bootstrap/cache
git commit -m "chore: add storage skeleton for CI" Pest 在配置的测试目录不存在时返回退出码 2(不是 1)。如果你的 phpunit.xml 列了 tests/Unit 但这个目录是空的、从来没提交过,CI 会在跑任何测试之前就失败。
解决办法不是加 .gitkeep,而是在 phpunit.xml 配置的每个目录里都放至少一个有实际意义的测试。CI 在跑测试之前就崩掉什么信息都没有;CI 在真实断言上失败才能告诉你哪里出了问题。
tests: name: Tests (PHP 8.4, MySQL 8.4) runs-on: ubuntu-latest needs: quality services: mysql: image: mysql:8.4 env: MYSQL_DATABASE: your_app_test MYSQL_ROOT_PASSWORD: secret ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v4 - name: Setup PHP 8.4 uses: shivammathur/setup-php@v2 with: php-version: '8.4' extensions: mbstring, pdo, pdo_mysql, bcmath, gd, zip, intl coverage: none tools: composer:v2 - name: Cache Composer dependencies uses: actions/cache@v4 with: path: api/vendor key: php-8.4-composer-${{ hashFiles('api/composer.lock') }} restore-keys: php-8.4-composer- - name: Install dependencies run: composer install --no-interaction --prefer-dist --no-progress - name: Copy .env run: cp .env.example .env.testing - name: Set test environment variables run: | echo "APP_ENV=testing" >> .env.testing echo "APP_KEY=base64:$(openssl rand -base64 32)" >> .env.testing echo "DB_CONNECTION=mysql" >> .env.testing echo "DB_HOST=127.0.0.1" >> .env.testing echo "DB_PORT=3306" >> .env.testing echo "DB_DATABASE=your_app_test" >> .env.testing echo "DB_USERNAME=root" >> .env.testing echo "DB_PASSWORD=secret" >> .env.testing echo "QUEUE_CONNECTION=sync" >> .env.testing echo "BROADCAST_CONNECTION=log" >> .env.testing echo "CACHE_STORE=array" >> .env.testing - name: Run migrations run: php artisan migrate --env=testing --force env: DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: "3306" DB_DATABASE: your_app_test DB_USERNAME: root DB_PASSWORD: secret - name: Run Pest tests run: ./vendor/bin/pest env: DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: "3306" DB_DATABASE: your_app_test DB_USERNAME: root DB_PASSWORD: secret 下篇讲怎么在测试之前加代码质量门禁,卡住风格漂移或者静态分析有问题的 PR。