1. 持续集成
  2. 词汇表
持续集成 / 介绍 / PHP 自动化测试

PHP 自动化测试

Laravel 8 框架使用 PHPUnit 9 作为默认的单元测试组件,适用于 PHP 7.3 及更高版本。

PHPUnit 可生成 HTML、Clover 等格式的测试覆盖率报告,JUnit 等格式的测试结果,但不支持检查测试覆盖率大小,可通过 「phpunit-coverage-check」进行检查。

安装

在代码目录执行命令进行安装:

$ composer require --dev phpunit/phpunit
$ composer require --dev rregeer/phpunit-coverage-check

编写测试代码

按照 Laravel 官方文档,编写测试代码。

业务代码(app/Http/Controllers/Welcome.php):

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class Welcome extends Controller
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request)
    {
        return view('welcome');
    }
}

路由(routes/web.php):

use Illuminate\Support\Facades\Route;

Route::get('/', \App\Http\Controllers\Welcome::class);

测试代码(app/Http/Controllers/Welcome.php):

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

本地运行

本地运行测试,将会生成「JUnit 格式测试结果」和「HTML 和 Clover 格式覆盖率报告」。

$ ./vendor/bin/phpunit --coverage-html storage/reports/tests/ --coverage-clover storage/reports/tests/clover.xml --log-junit storage/test-results/junit.xml tests/

$ head storage/test-results/junit.xml
$ open storage/reports/tests/index.html

根据 Clover 报告检查测试覆盖率,如果不达标,将会报错退出:

$ ./vendor/bin/coverage-check storage/reports/tests/clover.xml 80
  Total code coverage is 53.85 % which is below the accepted 80%
$ echo $?
  1

持续集成

带数据库的测试

自动化测试需要临时的基础设施(比如 MySQL、Redis、Elasticsearch),无需购买,启动多个 Docker 后台即可,测试完毕自动删除。

node {
  stage("检出") {
    checkout([
      $class: 'GitSCM',
      branches: [[name: GIT_BUILD_REF]],
      userRemoteConfigs: [[
        url: GIT_REPO_URL,
        credentialsId: CREDENTIALS_ID
    ]]])
  }
  stage('准备数据库') {
    sh 'docker network create bridge1'
    sh(script:'docker run --net bridge1 --name mysql -d -e "MYSQL_ROOT_PASSWORD=my-secret-pw" -e "MYSQL_DATABASE=test_db" mysql:5.7', returnStdout: true)
    sh(script:'docker run --net bridge1 --name redis -d redis:5', returnStdout: true)
  }
  docker.image('ecoding/php:8.0').inside("--net bridge1 -v \"${env.WORKSPACE}:/root/code\" -e 'APP_ENV=testing' -e 'DB_DATABASE=test_db'" +
      " -e 'DB_USERNAME=root' -e 'DB_PASSWORD=my-secret-pw' -e 'DB_HOST=mysql' -e 'REDIS_HOST=redis'" +
      " -e 'APP_KEY=base64:tbgOBtYci7i7cdx5RiFE3KZzUkRtJfbU3lbj5uPdL8U='") {
    sh 'composer install'

    stage('单元测试') {
      sh 'XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html storage/reports/tests/ --log-junit storage/test-results/junit.xml --coverage-text tests/'
      junit 'storage/test-results/junit.xml'
      codingHtmlReport(name: '测试覆盖率报告', path: 'storage/reports/tests/')
    }

    stage('生成 API 文档') {
      sh 'php artisan l5-swagger:generate'
      codingReleaseApiDoc(apiDocId: '1', apiDocType: 'specificFile', resultFile: 'storage/api-docs/api-docs.json')
    }
  }

  CODING_DOCKER_REG_HOST = "${CCI_CURRENT_TEAM}-docker.pkg.${CCI_CURRENT_DOMAIN}"
  CODING_DOCKER_IMAGE_NAME = "${env.PROJECT_NAME.toLowerCase()}/laravel-docker/laravel-demo"
  stage('构建 Docker 镜像') {
    if (env.TAG_NAME ==~ /.*/ ) {
      DOCKER_IMAGE_VERSION = "${env.TAG_NAME}"
    } else if (env.MR_SOURCE_BRANCH ==~ /.*/ ) {
      DOCKER_IMAGE_VERSION = "mr-${env.MR_RESOURCE_ID}-${env.GIT_COMMIT_SHORT}"
    } else {
      DOCKER_IMAGE_VERSION = "${env.BRANCH_NAME.replace('/', '-')}-${env.GIT_COMMIT_SHORT}"
    }
    // 本项目内的制品库已内置环境变量 CODING_ARTIFACTS_CREDENTIALS_ID,无需手动设置
    docker.withRegistry("https://${env.CCI_CURRENT_TEAM}-docker.pkg.coding.net", "${env.CODING_ARTIFACTS_CREDENTIALS_ID}") {
      docker.build("${CODING_DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_VERSION}").push()
    }
  }
}

不带数据库的测试

pipeline {
  agent {
    docker {
      image 'ecoding/php:8.0'
      reuseNode 'true'
      args '-v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker'
    }
  }
  stages {
    stage("检出") {
      steps {
        checkout([
          $class: 'GitSCM',
          branches: [[name: GIT_BUILD_REF]],
          userRemoteConfigs: [[
            url: GIT_REPO_URL,
            credentialsId: CREDENTIALS_ID
        ]]])
      }
    }
    stage('准备依赖') {
      steps {
        sh 'composer install'
      }
    }
    stage('单元测试') {
      post {
        always {
          junit 'storage/test-results/junit.xml'
        }
        success {
          codingHtmlReport(name: '测试覆盖率报告', path: 'storage/reports/tests/')
        }
      }
      steps {
        sh 'touch database/database.sqlite'
        sh 'XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html storage/reports/tests/ --log-junit storage/test-results/junit.xml --coverage-text tests/'
      }
    }
    stage('生成 API 文档') {
      steps {
        sh 'php artisan l5-swagger:generate'
        codingReleaseApiDoc(apiDocId: '1', apiDocType: 'specificFile', resultFile: 'storage/api-docs/api-docs.json')
      }
    }
    stage('构建 Docker 镜像') {
      steps {
        script {
          if (env.TAG_NAME ==~ /.*/ ) {
            DOCKER_IMAGE_VERSION = "${env.TAG_NAME}"
          } else if (env.MR_SOURCE_BRANCH ==~ /.*/ ) {
            DOCKER_IMAGE_VERSION = "mr-${env.MR_RESOURCE_ID}-${env.GIT_COMMIT_SHORT}"
          } else {
            DOCKER_IMAGE_VERSION = "${env.BRANCH_NAME.replace('/', '-')}-${env.GIT_COMMIT_SHORT}"
          }
          // 本项目内的制品库已内置环境变量 CODING_ARTIFACTS_CREDENTIALS_ID,无需手动设置
          docker.withRegistry("https://${env.CCI_CURRENT_TEAM}-docker.pkg.coding.net", "${env.CODING_ARTIFACTS_CREDENTIALS_ID}") {
            docker.build("${CODING_DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_VERSION}").push()
          }
        }
      }
    }
  }
  environment {
    CODING_DOCKER_REG_HOST = "${CCI_CURRENT_TEAM}-docker.pkg.${CCI_CURRENT_DOMAIN}"
    CODING_DOCKER_IMAGE_NAME = "${env.PROJECT_NAME.toLowerCase()}/laravel-docker/laravel-demo"
  }
}

运行结果

通过持续集成任务:

输出测试报告:

输出通用报告:

报告详情:

上一篇Java 自动化测试
最近更新
感谢反馈有用
感谢反馈没用

在阅读中是否遇到以下问题?

您希望我们如何改进?