测试 Dancer

舞蹈比赛的舞者和评委

Dancer(及其他)PSGI 应用程序的作者很可能习惯于测试Plack::Test,虽然这是一个崇敬的选择,这是很裸机。

在去年出现的过程中,我写了一篇关于Test::Mojo的文章,展示了许多简单易用(我敢说)有趣的方法,你可以用它来测试你的Mojolicious应用程序。如果你错过了,那就去看看吧

我希望至少有一些人能够阅读并思考,“我很乐意使用它,但我不会使用Mojolicious!”; 好吧,你很幸运!只需要一点角色来弥补差距,你也可以使用Test::Mojo来测试你的PSGI应用程序!

安装PSGI应用程序

Mojolicious本身不使用PSGI协议,因为它没有提供某些特性,而且某些异步操作也是必需的。也就是说,你可以使用Mojo::Server::PSGI在PSGI服务器上提供Mojolicious应用程序。当你的基于Mojolicious的应用程序检测到它已在PSGI服务器(例如plackup或Starman)下启动时,将自动使用此Mojolicious核心模块。

虽然在Mojo应用程序和PSGI服务器之间进行转换是核心功能,但相反,在PSGI应用程序和Mojolicious服务器(或应用程序,如你所见)之间进行转换可作为第三方模块使用。Mojolicious::Plugin::MountPSGI,顾名思义,可以将PSGI应用程序安装到基于Mojolicious的应用程序中。为此,它构建了一个新的,空的Mojolicious应用程序,在将任何请求转移到PSGI环境之前,将其转移到任何mount-ed应用程序。

使用Test::Mojo进行测试

一旦你能做到这一点,采用PSGI应用程序,用MountPSGI包装它,并将其设置为与Test::Mojo一起使用的应用程序是微不足道的。尽管如此,为了让它变得更加容易,在Test::Mojo::Role::PSGI中已经完成了所有这些工作。

与任何Mojolicious Role一样,我们可以使用应用角色with_roles创建(主要是匿名)子类。你可以使用快捷方式+代替Test::Mojo::Role::

1
2
use Test::Mojo;
my $class = Test::Mojo->with_roles('+PSGI');

然后使用PSGI应用程序的路径实例化该角色,或者使用PSGI应用程序本身。

由于你使用的是角色,这些角色都与组合有关,因此你还可以应用可能在CPAN上找到的其他角色。

一个例子

作为一个例子,假设我们有一个简单的应用程序脚本(命名app.psgi),可以以不同的方式渲染 "hello world""hello $user" 。我将允许纯文本响应,JSON和模板化HTML(使用简单的模板来保持这种简洁)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Dancer2;

set template => 'simple';
set views => '.';

any '/text' => sub {
my $name = param('name') // 'world';
send_as plain => "hello $name";
};

any '/data' => sub {
my $name = param('name') // 'world';
send_as JSON => { hello => $name };
};

any '/html' => sub {
my $name = param('name') // 'world';
template 'hello' => { name => $name };
};

start;

而模板(hello.tt)是

1
2
3
4
<dl id="data">
<dt id="hello">hello</dt>
<dd><% name %></dd>
</dl>

dldtdd标签是一种在 HTML 中标记键-值对语义的方式,因此它是几乎和上面的JOSN格式一样。我已经构建的HTML,虽然用于显示很好,但以编程方式查询就不友好了,这对于示例来说是故意的。

测试

当然,我们可以用 plackup 启动应用程序,但这不是我们想要做的。我会稍微分解下测试脚本,但如果你想看到这些文件中的任何一个,请查看博客仓库以获取完整列表。相反,让我们将其加载到测试脚本中。

1
use Mojo::Base -strict;

现在,如果你不熟悉,use Mojo::Base -strict可以快速说出来

1
2
3
4
5
use strict;
use warnings;
use utf8;
use IO::Handle;
use feature ':5.10';

但节省了大量的打字。接下来,我们加载必要的测试库。然后创建一个组合了 PSGI角色的 Test::Mojo实例,并创建一个指向我们要测试的应用程序的新实例。

1
2
3
use Test::More;
use Test::Mojo;
my $t = Test::Mojo->with_roles('+PSGI')->new('app.psgi');

有了这个,继续测试!在我们的第一次测试中,我们将专注于纯文本端点/text

1
2
3
4
$t->get_ok('/text')
->status_is(200)
->content_type_like(qr[text/plain])
->content_is('hello world');

上述每个方法调用都是一个测试。第一个,get_ok构建一个事务并请求资源。由于url是相对的,它由app处理的(如果我们想,我们可以使用完全限定的url请求和web资源)。事务存储在tester对象($t)中,所有后续测试将检查它,直到它被下一个请求替换。

剩下的测试是相当不言自明的,我们检查响应状态是200,我们得到了一个我们期望的内容类型标题,其内容是我们期望的。内容已经被utf-8解码,并且脚本拥有隐式的 use utf8,所以如果你期望 unicode,你可以轻松地比较它们。该测试返回测试对象,因此可以进行链接,从而实现视觉上干净的测试集。

下一个测试类似,但是这个测试使用标准的Mojo::UserAgent样式请求生成一个查询字符串,为我们的问候语命名为 Santa。测试是完全相同的,当然它检查内容是否迎问候了 Santa。

1
2
3
4
$t->get_ok('/text', form => { name => 'santa' })
->status_is(200)
->content_type_like(qr[text/plain])
->content_is('hello santa');

继续我们请求数据端点,无论是否有查询,然后类似地测试响应。

1
2
3
4
5
6
7
8
9
$t->get_ok('/data')
->status_is(200)
->content_type_like(qr[application/json])
->json_is('/hello' => 'world');

$t->post_ok('/data' => form => { name => 'rudolph' })
->status_is(200)
->content_type_like(qr[application/json])
->json_is('/hello' => 'rudolph');

你可以看到我们使用该json_is方法来测试响应。现在,测试本来可能是->json_is({hello => 'rudolph'})想要测试整个文档。通过传递JSON指针,我只能检查我感兴趣的部分。

最后,我将测试HTML端点。正如我上面所说,结果不容易解析。我们想要测试带有id 的dd标签后面的标签内容,所有这些内容都在带有id 的标签内。那将是一个可怕的正则表达式(呵呵)。然而,使用CSS选择器就是一小菜一碟了。

1
2
3
4
5
6
7
8
9
10
11
$t->get_ok('/html')
->status_is(200)
->content_type_like(qr[text/html])
->text_is('dl#data dt#hello + dd', 'world');

$t->post_ok('/html' => form => { name => 'grinch' })
->status_is(200)
->content_type_like(qr[text/html])
->text_is('dl#data dt#hello + dd', 'grinch');

done_testing;

在今年的Mojolicious Advent 日历中,我们已经看到CSS选择器 强大功能的一些好例子,所以我不会详细介绍。然而,重点仍然是,使用CSS选择器测试HTML响应允许你编写更多更好测试的方式使测试成为目标,因为你不必破解提取所需的部分。

测试WebSockets

好的,这一切都很好,但当然现在它已经到了你们一直在等待的地步:你能测试一下WebSockets吗?正如Jason Crome在他 Twelve Days of Dancer 中所提到的,你现在可以通过Dancer2::Plugin::WebSocket与WebSockets 共舞,那么Test::Mojo可以测试它们吗?

好吧,到目前为止还没有用过我上面展示的角色。这可能是可能的,但它会涉及学习深度的PSGI魔法,我不确定我是否足够聪明; 补充显然很受欢迎 :D。

我上面还提到Test::Mojo可以通过完全限定的URL测试它可以访问的任何内容,所以让我们启动一个服务器并测试它!我将使用与插件捆绑在一起的示例来实现简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Mojo::Base -strict;

use EV;
use Test::More;
use Test::Mojo;

use Twiggy::Server;
use Plack::Util;

my $app = Plack::Util::load_psgi('bin/app.psgi');
my $url;
my $twiggy = Twiggy::Server->new(
host => '127.0.0.1',
server_ready => sub {
my $args = shift;
$url = "ws://$args->{host}:$args->{port}/ws";
},
);
$twiggy->register_service($app);

这启动Twiggy绑定到随机端口上的localhost并使用它启动应用程序。当服务器启动时,实际的主机和端口将传递给server_ready我们用于构建测试URL 的回调。现在你只是正常创建一个Test::Mojo实例,但这次打开一个websocket到我们上面构建的完全限定的url上。

1
2
3
4
5
6
7
8
9
my $t = Test::Mojo->new;

$t->websocket_ok($url)
->send_ok({json => {hello => 'Dancer'}})
->message_ok
->json_message_is({hello => 'browser!'})
->finish_ok;

done_testing;

与前面的示例不同,这次连接在方法调用之间保持打开(但被阻止)。根据示例的协议,我们首先将问候语作为JSON文档发送给Dancer应用程序。由于现实世界的websocket使用只是序列化的JSON消息,Mojolicious提供了许多JSON-over-WebSocket便利。一个这样的便利是虚拟websocket框架类型,它采用数据结构并在将其实际作为文本框架发送之前将其序列化为JSON。

然后我们等待收到回复的消息message_ok。在这种情况下,我们希望应用程序通过称我们为“浏览器!”来迎接我们。哦,它不知道更好!我们可以测试JSON回复json_message_isjson_is如上所述,但对于websocket消息)。最后,我们关闭连接,测试它是否正确关闭。

即使从Dancer应用程序测试WebSockets也很容易!

结论

虽然PSGI空间中有一些很棒的测试选项,但Test::Mojo对Dancer和PSGI用户有很多好处。通过使用Test::Mojo::Role::PSGI或运行本地绑定的服务器,Test::Mojo可以是任何PSGI开发人员工具箱中的工具。