# 测试控制器

新增的辅助类与 Trait 使测试控制器更加便捷。测试控制器时，可直接运行控制器内部代码，无需经历完整的应用引导流程。通常使用 功能测试工具 会更简便，但若有特殊需求，也可使用此功能。

备注

由于未引导整个框架，某些情况下可能无法以此方式测试控制器。

## 辅助 Trait

要启用控制器测试，需在测试中使用 `ControllerTestTrait`：

    <?php

    namespace App\Controllers;

    use CodeIgniter\Test\CIUnitTestCase;
    use CodeIgniter\Test\ControllerTestTrait;
    use CodeIgniter\Test\DatabaseTestTrait;

    class FooControllerTest extends CIUnitTestCase
    {
        use ControllerTestTrait;
        use DatabaseTestTrait;
    }

引入此 Trait 后，即可开始设置环境，包括请求和响应类、请求体、URI 等。通过 `controller()` 方法指定要使用的控制器，传入控制器的完全限定类名。最后，调用 `execute()` 方法，将方法名作为参数传入：

    <?php

    namespace App\Controllers;

    use CodeIgniter\Test\CIUnitTestCase;
    use CodeIgniter\Test\ControllerTestTrait;
    use CodeIgniter\Test\DatabaseTestTrait;

    class ForumControllerTest extends CIUnitTestCase
    {
        use ControllerTestTrait;
        use DatabaseTestTrait;

        public function testShowCategories()
        {
            $result = $this->withUri('http://example.com/categories')
                ->controller(ForumController::class)
                ->execute('showCategories');

            $this->assertTrue($result->isOK());
        }
    }

## 辅助方法

### controller($class)

指定要测试的控制器类名。第一个参数必须是完全限定类名（即包含命名空间）：

    <?php

    $this->controller(\App\Controllers\ForumController::class);

### execute(string $method, ...$params)

在控制器内执行指定方法。第一个参数是要运行的方法名：

    <?php

    $results = $this->controller(\App\Controllers\ForumController::class)
        ->execute('showCategories');

通过指定第二个及后续参数，可将其传递给控制器方法。

此方法返回一个新的辅助类，提供多种检查响应的例程。详见下文。

### withConfig($config)

支持传入修改后的 app/Config/App.php，以测试不同设置：

    <?php

    $config              = new \Config\App();
    $config->appTimezone = 'America/Chicago';

    $results = $this->withConfig($config)
        ->controller(\App\Controllers\ForumController::class)
        ->execute('showCategories');

如未提供，将使用应用的 App 配置文件。

### withRequest($request)

可根据测试需求提供定制的 IncomingRequest 实例：

    <?php

    $request = new \CodeIgniter\HTTP\IncomingRequest(
        new \Config\App(),
        new \CodeIgniter\HTTP\URI('http://example.com'),
        null,
        new \CodeIgniter\HTTP\UserAgent(),
    );

    $request->setLocale($locale);

    $results = $this->withRequest($request)
        ->controller(\App\Controllers\ForumController::class)
        ->execute('showCategories');

如未提供，将创建一个使用应用默认值的全新 IncomingRequest 实例并传入控制器。

### withResponse($response)

可提供 Response 实例：

    <?php

    $response = new \CodeIgniter\HTTP\Response(new \Config\App());

    $results = $this->withResponse($response)
        ->controller(\App\Controllers\ForumController::class)
        ->execute('showCategories');

如未提供，将创建一个使用应用默认值的全新 Response 实例并传入控制器。

### withLogger($logger)

可提供 Logger 实例：

    <?php

    $logger = new \CodeIgniter\Log\Handlers\FileHandler();

    $results = $this->withResponse($response)
        ->withLogger($logger)
        ->controller(\App\Controllers\ForumController::class)
        ->execute('showCategories');

如未提供，将创建一个使用默认配置值的全新 Logger 实例并传入控制器。

### withUri(string $uri)

用于提供新 URI，模拟控制器运行时的客户端访问 URL。如需在控制器内检查 URI 片段，此方法非常有用。唯一参数是代表有效 URI 的字符串：

    <?php

    $results = $this->withUri('http://example.com/forums/categories')
        ->controller(\App\Controllers\ForumController::class)
        ->execute('showCategories');

测试时始终提供 URI 是良好的实践，可避免意外情况。

备注

自 v4.4.0 起，此方法会创建带 URI 的新 Request 实例。 因为 Request 实例应包含 URI 实例。此外，如果 URI 字符串中的主机名 与 `Config\App` 不匹配，将设置有效的主机名。

### withBody($body)

可自定义请求体。测试 API 控制器时，如需将 JSON 数据设为请求体，此功能非常实用。唯一参数是代表请求体的字符串：

    <?php

    $body = json_encode(['foo' => 'bar']);

    $results = $this->withBody($body)
        ->controller(\App\Controllers\ForumController::class)
        ->execute('showCategories');

## 检查响应

`ControllerTestTrait::execute()` 返回 `TestResponse` 实例。有关如何在测试用例中使用此类执行额外断言和验证，请参阅 Testing Responses。

## 测试过滤器

与控制器测试类似，框架提供的工具可用于测试自定义 过滤器 及其在路由中的应用。

### 辅助 Trait

与控制器测试一样，需在测试用例中引入 `FilterTestTrait` 以启用这些功能：

    <?php

    namespace App\Filters;

    use CodeIgniter\Test\CIUnitTestCase;
    use CodeIgniter\Test\FilterTestTrait;

    class FooFilterTest extends CIUnitTestCase
    {
        use FilterTestTrait;
    }

### 配置

由于与控制器测试逻辑重合，`FilterTestTrait` 可与 `ControllerTestTrait` 搭配使用，方便在同一个类中同时调用。 引入此 Trait 后，`CIUnitTestCase` 会自动检测其 `setUp` 方法，并准备测试所需的所有组件。如需特殊配置，可在调用支持方法前修改相关属性：

- `$request`：已就绪的默认 `IncomingRequest` 服务实例

- `$response`：已就绪的默认 `ResponseInterface` 服务实例

- `$filtersConfig`：默认 `Config\Filters` 配置（注：自动发现由 `Filters` 类处理，因此此处不包含模块别名）

- `$filters`：使用上述三个组件生成的 `CodeIgniter\Filters\Filters` 实例

- `$collection`：已就绪的 `RouteCollection` 实例，包含对 `Config\Routes` 的自动发现

默认配置最接近真实运行的项目，通常是测试的首选。但若需模拟过滤器在未配置过滤器的路由上意外触发等情况，可将其手动添加到配置中：

    <?php

    namespace App\Filters;

    use CodeIgniter\Test\CIUnitTestCase;
    use CodeIgniter\Test\FilterTestTrait;

    final class FooFilterTest extends CIUnitTestCase
    {
        use FilterTestTrait;

        protected function testFilterFailsOnAdminRoute()
        {
            $this->filtersConfig->globals['before'] = ['admin-only-filter'];

            $this->assertHasFilters('unfiltered/route', 'before');
        }

        // ...
    }

### 检查路由

第一个辅助方法 `getFiltersForRoute()` 用于模拟指定路由，并返回在特定位置（“before” 或 “after”）本应运行的所有过滤器别名列表。由于该方法不会实际执行任何控制器或路由代码，其性能表现远胜于控制器测试与 HTTP 测试。

getFiltersForRoute($route, $position)
  参数:

    - $route (`string`) -- 要检查的 URI

    - $position (`string`) -- 要检查的过滤器方法，"before" 或 "after"

  返回:

    将运行的每个过滤器的别名

  返回类型:

    `string[]`

  使用示例：

      <?php

      $result = $this->getFiltersForRoute('/', 'after'); // ['toolbar']

### 调用过滤器方法

配置中描述的属性均已设置，以确保在不干扰其他测试的前提下实现最佳性能。下一个辅助方法将使用这些属性返回一个可调用的方法，以安全地测试过滤器代码并检查结果。

getFilterCaller($filter, $position)
  参数:

    - $filter (`FilterInterface|string`) -- 过滤器实例、类或别名

    - $position (`string`) -- 要运行的过滤器方法，"before" 或 "after"

  返回:

    运行模拟过滤器事件的可调用方法

  返回类型:

    `Closure`

  使用示例：

      <?php

      namespace App\Filters;

      use CodeIgniter\Test\CIUnitTestCase;
      use CodeIgniter\Test\FilterTestTrait;

      final class FooFilterTest extends CIUnitTestCase
      {
          use FilterTestTrait;

          protected function testUnauthorizedAccessRedirects()
          {
              $caller = $this->getFilterCaller('permission', 'before');
              $result = $caller('MayEditWidgets');

              $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result);
          }
      }

  注意 `Closure` 可接受输入参数，这些参数会传递给过滤器方法。

### 断言

除上述辅助方法外，`FilterTestTrait` 还提供一些断言，可简化测试方法。

#### assertFilter()

`assertFilter()` 方法检查指定位置的给定路由是否使用了该过滤器（通过别名）：

    <?php

    // Make sure users are logged in before checking their account
    $this->assertFilter('users/account', 'before', 'login');

#### assertNotFilter()

`assertNotFilter()` 方法检查指定位置的给定路由是否未使用该过滤器（通过别名）：

    <?php

    // Make sure API calls do not try to use the Debug Toolbar
    $this->assertNotFilter('api/v1/widgets', 'after', 'toolbar');

#### assertHasFilters()

`assertHasFilters()` 方法检查指定位置的给定路由是否至少设置了一个过滤器：

    <?php

    // Make sure that filters are enabled
    $this->assertHasFilters('filtered/route', 'after');

#### assertNotHasFilters()

`assertNotHasFilters()` 方法检查指定位置的给定路由是否未设置任何过滤器：

    <?php

    // Make sure no filters run for our static pages
    $this->assertNotHasFilters('about/contact', 'before');