资讯专栏INFORMATION COLUMN

写 Laravel 测试代码(五)

xbynet / 2991人阅读

摘要:写一个,的是,的内容参照写测试代码三,然后写上很明显,这里测试的是,即和,是一个自定义的,主要功能就是实现了全部,并保存在文件里作为。

本文主要探讨写laravel integration/functional test cases时候,如何assert。前面几篇文章主要聊了如何reseed测试数据,mock数据,本篇主要聊下assert的可行实践,尽管laravel官方文档聊了Testing JSON APIs,并提供了一些辅助的assert方法,如assertStatus(), assertJson()等等,但可行不实用,不建议这么做。

最佳需要是对api产生的response做更精细的assert。那如何是更精细的assertion?简单一句就是把response code/headers/content 完整内容进行比对(assert)。 方法就是把response的内容存入json文件里作为baseline。OK,接下来聊下如何做。

写一个AccountControllerTest,call的是/api/v1/accounts,AccountController的内容参照写Laravel测试代码(三),然后写上integration/functional test cases:

assertApiIndex();
    }

    public function testShow()
    {
        $this->assertApiShow(1);
    }
}

很明显,这里测试的是index/show api,即/api/v1/accounts和/api/v1/accounts/{account_id},AssertApiBaseline是一个自定义的trait,主要功能就是实现了assert 全部response,并保存在json文件里作为baseline。所以,重点就是AssertApiBaseline该如何写,这里就直接贴代码:

 [
            "D" => "DiJeb7IQHo8FOFkXulieyA",
        ],
        "api" => [
        ],
    ];

    private static $servers = [
        "web" => [
            "HTTP_ACCEPT"  => "application/json",
            "HTTP_ORIGIN"  => "https://test.company.com",
            "HTTP_REFERER" => "https://test.company.com",
        ],
        "api" => [
            "HTTP_ACCEPT" => "application/json",
        ],
    ];

    public static function assertJsonResponse(TestResponse $response, string $message = "", array $ignores = []): TestResponse
    {
        static::assertJsonResponseCode($response, $message);
        static::assertJsonResponseContent($response, $message);
        static::assertJsonResponseHeaders($response, $message);

        return $response;
    }

    public static function assertJsonResponseCode(TestResponse $response, string $message = ""): void
    {
        static::assert($response->getStatusCode(), $message);
    }

    public static function assertJsonResponseContent(TestResponse $response, string $message = "", array $ignores = []): void
    {
        static::assert($response->json(), $message);
    }

    public static function assertJsonResponseHeaders(TestResponse $response, string $message = ""): void
    {
        $headers = $response->headers->all();

        $headers = array_except($headers, [
            "date",
            "set-cookie",
        ]); // except useless headers

        static::assert($headers, $message);
    }

    public static function assert($actual, string $message = "", float $delta = 0.0, int $maxDepth = 10, bool $canonicalize = false, bool $ignoreCase = false): void
    {
        // assert $actual with $expected which is from baseline json file
        // if there is no baseline json file, put $actual data into baseline file (or -d rebase)
        // baseline file path
        // support multiple assertion in a test case

        static $assert_counters = [];
        static $baselines       = [];

        $class     = get_called_class();
        $function  = static::getFunctionName(); // "testIndex"
        $signature = "$class::$function";

        if (!isset($assert_counters[$signature])) {
            $assert_counters[$signature] = 0;
        } else {
            $assert_counters[$signature]++;
        }

        $test_id = $assert_counters[$signature];

        $baseline_path = static::getBaselinesPath($class, $function);

        if (!array_key_exists($signature, $baselines)) {
            if (file_exists($baseline_path) && array_search("rebase", $_SERVER["argv"], true) === false) { // "-d rebase"
                $baselines[$signature] = GuzzleHttpjson_decode(file_get_contents($baseline_path), true);
            } else {
                $baselines[$signature] = [];
            }
        }

        $actual = static::prepareActual($actual);

        if (array_key_exists($test_id, $baselines[$signature])) {
            static::assertEquals($baselines[$signature][$test_id], $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
        } else {
            $baselines[$signature][$test_id] = $actual;

            file_put_contents($baseline_path, GuzzleHttpjson_encode($baselines[$signature], JSON_PRETTY_PRINT));

            static::assertTrue(true);

            echo "R";
        }
    }

    /**
     * @param string|string[]|null  $route_parameters
     * @param array $parameters
     *
     * @return mixed
     */
    protected function assertApiIndex($route_parameters = null, array $parameters = [])
    {
        return static::assertApiCall("index", $route_parameters ? (array) $route_parameters : null, $parameters);
    }

    protected function assertApiShow($route_parameters, array $parameters = [])
    {
        assert($route_parameters !== null, "$route_parameters cannot be null");

        return static::assertApiCall("show", (array) $route_parameters, $parameters);
    }

    protected static function getFunctionName(): string
    {
        $stacks = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);

        do {
            $stack = array_pop($stacks);
        } while ($stack && substr($stack["function"], 0, 4) !== "test");

        return $stack["function"]; // "testList"
    }

    protected static function getBaselinesPath(string $class, string $function): string
    {
        $class = explode("", $class);

        $dir = implode("/", array_merge(
            [strtolower($class[0])],
            array_slice($class, 1, -1),
            ["_baseline", array_pop($class)]
        ));

        if (!file_exists($dir)) {
            mkdir($dir, 0755, true);
        }

        return base_path() . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR . $function . ".json";
    }

    protected static function prepareActual($actual)
    {
        if ($actual instanceof Arrayable) {
            $actual = $actual->toArray();
        }

        if (is_array($actual)) {
            array_walk_recursive($actual, function (&$value, $key): void {
                if ($value instanceof Arrayable) {
                    $value = $value->toArray();
                } elseif ($value instanceof Carbon) {
                    $value = "Carbon:" . $value->toIso8601String();
                } elseif (in_array($key, ["created_at", "updated_at", "deleted_at"], true)) {
                    $value = Carbon::now()->format(DATE_RFC3339);
                }
            });
        }

        return $actual;
    }

    private function assertApiCall(string $route_action, array $route_parameters = null, array $parameters = [])
    {
        [$uri, $method] = static::resolveRouteUrlAndMethod(static::resolveRouteName($route_action), $route_parameters);

        /** @var IlluminateFoundationTestingTestResponse $response */
        $response = $this->call($method, $uri, $parameters, $this->getCookies(), [], $this->getServers(), null);

        return static::assertJsonResponse($response, "");
    }

    private static function resolveRouteName(string $route_action): string
    {
        return static::ROUTE_NAME . "." . $route_action;
    }

    private static function resolveRouteUrlAndMethod(string $route_name, array $route_parameters = null)
    {
        $route = Route::getRoutes()->getByName($route_name);
        assert($route, "Route [$route_name] must be existed.");

        return [route($route_name, $route_parameters), $route->methods()[0]];
    }

    private function getCookies(array $overrides = []): array
    {
        $cookies = $overrides + self::$cookies[static::$middlewareGroup];

        return $cookies;
    }

    private function getServers(array $overrides = []): array
    {
        return $overrides + self::$servers[static::$middlewareGroup];
    }
}

虽然AssertApiBaseline有点长,但重点只有assert()方法,该方法实现了:

如果初始没有baseline文件,就把response内容存入json文件

如果有json文件,就拿baseline作为expected data,来和本次api产生的response内容即actual data做assertion

如果有"rebase"指令表示本次api产生的response作为新的baseline存入json文件中

支持一个test case里执行多次assert()方法

所以,当执行phpunit指令后会生成对应的baseline文件:

OK,首次执行的时候重新生成baseline文件,查看是不是想要的结果,以后每次改动该api后,如果手滑写错了api,如response content是空,这时候执行测试时会把baseline作为expected data和错误actual data 进行assert就报错,很容易知道代码写错了;如果git diff知道最新的response 就是想要的(如也无需求需要把"name"换另一个),就phpunit -d rebase 把新的response作为新的baseline就行。。

这比laravel文档中说明的写json api test cases的优点在哪?就是对response做了精细控制。对response 的status code,headers,尤其是response content做了精细控制(content的每一个字段都行了assert对比)。
这是我们这边写api test cases的实践,有疑问可留言交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30695.html

相关文章

  • Laravel 菜鸟晋级之路

    摘要:用也有三四个月了,虽然是兼职开发,但是使用的频率非常之高,毕竟是产品化的一个项目。第二阶段数据库和开发了比较多的功能之后,会发现需要大量的测试数据,这时候和就该大显身手了。 用Laravel也有三四个月了,虽然是兼职开发,但是使用的频率非常之高,毕竟是产品化的一个项目。在这期间,也踩了无数的坑,走了很多弯路,所以准备把最近的感悟记录下来,方便后来者。 第一阶段:简单的增删改查 这是最...

    YacaToy 评论0 收藏0
  • 最适合入门的Laravel初级教程()

    摘要:最适合入门的初级教程五路由咱会创建了控制器也有了接下来要搞的就是把两者关联起来了最适合入门的初级教程三我们讲过的或者方法第一个参数就是我们要定义的路由就是我们在地址栏请求的那段第二个参数可以是一个闭包函数里面写请求定义的路由时执行的内容上篇 最适合入门的Laravel初级教程(五) 路由咱会创建了; 控制器也有了;接下来要搞的就是把两者关联起来了;最适合入门的laravel初级教程(三...

    kamushin233 评论0 收藏0
  • 13 个快速构建 Laravel 后台的扩展包

    摘要:值得一提的是扩展包不免费用于商业用途,作者用一种人类友好的方式说你使用这个扩展包就是应该去挣钱的,而不是免费的去工作这个扩展包收费美元。除了这些,还有五个没有全面的审查的扩展包。最后,还有三个优质的包选择于。 showImg(https://segmentfault.com/img/remote/1460000012312105?w=2200&h=1125); 开发者们都是懒惰的,不,...

    MiracleWong 评论0 收藏0
  • 13 个快速构建 Laravel 后台的扩展包

    摘要:值得一提的是扩展包不免费用于商业用途,作者用一种人类友好的方式说你使用这个扩展包就是应该去挣钱的,而不是免费的去工作这个扩展包收费美元。除了这些,还有五个没有全面的审查的扩展包。最后,还有三个优质的包选择于。 showImg(https://segmentfault.com/img/remote/1460000012312105?w=2200&h=1125); 开发者们都是懒惰的,不,...

    ityouknow 评论0 收藏0
  • Laravel 5系列教程:MVC的基本流程

    摘要:原文来自免费视频教程地址期间受到很多私事影响,终于还是要好好写写的教程了。我们来实现这个功能显示文章详情通过文章展示来快速体验上面的流程注册路由来到中,我们增加一个路由上面的路由指定我们需要加载中的方法。 原文来自: https://jellybool.com/post/programming-with-laravel-5-model-controller-view-basic-wor...

    mrcode 评论0 收藏0

发表评论

0条评论

xbynet

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<