是否可以在perl中模拟或修补输入参数?

Gan*_*ang 2 perl unit-testing mocking

似乎在python中,mock.patch可以修补输入

是否可以在perl中模拟输入参数?如何?这是输出:

not ok 1 - expect: 59, got: 1
Run Code Online (Sandbox Code Playgroud)

我到目前为止的代码:

#!/usr/bin/perl
use Test::More;
use Test::MockObject;
use Date::Calc;

# scope of mocked Date::Calc
{
    my $import;
    my $mock = Test::MockObject->new();
    $mock->fake_module('Date::Calc', import => sub { $import = caller} );
    $mock->fake_new('Date::Calc');
    $mock->mock('Days_in_Year', sub {print "how to mock parameter - month to be 2\n"});
    # is it possible to mock a parameter? how if possible?
    my $days_mock = $mock->Days_in_Year(2015,6);
    ok($days_mock == 59, "expect: 59, got: $days_mock\n");
}

# unmocked module and methods
my $days_in_year = Date::Calc::Days_in_Year(2015,6);
ok($days_in_year == 181, "expect: 181, got: $days_in_year\n");
done_testing(2);
Run Code Online (Sandbox Code Playgroud)

sim*_*que 5

你不能模拟一个变量,你必须模拟一个依赖.该依赖关系可以是子,对象,甚至可以是整个RDBMS.问题中的代码看起来像是一个证明模拟工作的测试,因此我将尝试在我的示例中坚持使用它.

模拟对象时,称为依赖注入.我将在后面的答案中讨论这个问题.

出于空间和懒惰的原因,请假设此答案中的每一段代码都以下列内容开头:

use strict; 
use warnings;
Run Code Online (Sandbox Code Playgroud)

嘲笑一个子

有时,由于代码的设计方式,这是不可能的.在这种情况下,您需要模拟一个函数(在Perl中是一个子函数)或两个,或者可能是整个模块.后者几乎不需要.

覆盖单个子(或几个)的最简单方法是Sub :: Override.它对单元测试很有用,但不限于此.

use Test::More;
use Sub::Override;
use DateTime;

my $dt = DateTime->now;

{   # scoped in this block
    my $override = Sub::Override->new( 'DateTime::year', sub { 2015 } );
    is $dt->year, 2015, 'Year is 2015';
}

isnt $dt->year, 2015, 'Year is NOT 2015';

done_testing;

__END__
ok 1 - Year is 2015
ok 2 - Year is NOT 2015
1..2
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,sub被覆盖了,但仅限于给定的范围.这非常有用,因为它快速,易记,易读,这是一个非常重要的考虑因素.这是一个简单的例子.

package Foo;
require Weird::Legacy::Dependency;

sub hello {
    my $name = shift;
    my $hi = Weird::Legacy::Dependency::rnd_salutation();
    return "$hi, $name";
}
Run Code Online (Sandbox Code Playgroud)

在我们需要测试的代码中,有一个可怕的遗留依赖,我们无法重构.作者喜欢spagetti,这些东西很难辨认.它可能返回这样的东西:

Hi, Bob
Hallo, Bob
Good Afternoon, Bob
?????, Bob
Run Code Online (Sandbox Code Playgroud)

那我们该怎么处理呢?当然,我们覆盖了sub rnd_salutation.

use Test::More;
use Sub::Override;

{   # scoped in this block
    my $override = Sub::Override->new(
      'Weird::Legacy::Dependency::rnd_salutation', sub { 'Hi' } );
    is Foo::hello('Joe'), 'Hi, Joe', 'Say hi to Joe';
}
Run Code Online (Sandbox Code Playgroud)

现在,我们可以确保hello不正是<given_salutation>, $name 我们虽然不知道是什么样的随机的东西是走出传统的功能.

嘲弄一个物体

如果您的代码是面向对象的,则可以使每个依赖项都可注入.这样,你可以控制更多.一个非常典型的例子是数据库连接或LWP对象.这是一个简化的例子.这段代码可能是某些API的成熟客户端.

package My::WebserviceClient;
use Moose;
use LWP::UserAgent;

has ua => (
    is      => 'ro',
    isa     => 'LWP::UserAgent',
    default => sub { LWP::UserAgent->new },
);

sub call {
    my ($self, $url) = @_;

    my $res = $self->ua->get($url);
    return $res->content if $res->is_success;
}

package main;

my $client = My::WebserviceClient->new;
print length $client->call('http://www.example.org');
Run Code Online (Sandbox Code Playgroud)

现在来测试一下,我们不希望它真正去取东西.所以我们需要嘲笑它.让我们创建一个具有方法并返回固定HTTP :: Response模拟对象.get

use Test::More;
use Test::MockObject;
use HTTP::Response;
use My::WebserviceClient;

# prepare the mock object
my $mock = Test::MockObject->new;
$mock->set_isa('LWP::UserAgent');

# set up a response object
my $res = HTTP::Response->new( 200, 'OK', [], 'Hello' );
$mock->set_always( 'get', $res );

# here we INJECT the DEPENDENCY
my $client = My::WebserviceClient->new( ua => $mock );

is $client->call('http://www.example.org/'), 'Hello', 
  'Just the content is returned';

done_testing;

__END__
ok 1 - Just the content is returned
Run Code Online (Sandbox Code Playgroud)

这将起作用,因为get模拟用户代理中的方法现在总是返回我们准备好的HTTP :: Response对象.这样,我们还可以测试程序是否正确处理404重复.

同时嘲笑这两个

但有时候不可能注入依赖.如果该程序的作者过于懒惰1为用户代理设置属性并执行此操作该怎么办?

package My::WebserviceClient;
use Moose;
use LWP::UserAgent;

sub call {
    my ( $self, $url ) = @_;

    my $res = LWP::UserAgent->new->get($url);
    return $res->content if $res->is_success;
}
Run Code Online (Sandbox Code Playgroud)

现在注射不再起作用了.我们需要做点别的事.Test :: MockObject不鼓励使用它的fake_module方法,因为Test :: MockModule可以做得更好.我们需要使用它来模拟newLWP :: UserAgent中的方法,因此它返回我们之前在测试中所做的模拟用户代理对象.

use Test::More;
use Test::MockObject;
use Test::MockModule;
use HTTP::Response;
use My::WebserviceClient;

# prepare the mock object
my $mock_ua = Test::MockObject->new;
$mock_ua->set_isa('LWP::UserAgent');

# set up a response object
my $res = HTTP::Response->new( 200, 'OK', [], 'Hello' );
$mock_ua->set_always( 'get', $res );

# Now we need to mock LWP::UserAgent's new to return our
# mocked object
{
    my $module = Test::MockModule->new('LWP::UserAgent');
    $module->mock( 'new', sub { return $mock_ua } );

    my $client = My::WebserviceClient->new;
    # inside of call, it will now use our mocked LWP::UA::new
    is $client->call('http://www.example.org/'), 'Hello', 
      'Just the content is returned';
}

done_testing;

__END__
ok 1 - Just the content is returned
Run Code Online (Sandbox Code Playgroud)

当然在这种情况下我们也可以使用Sub :: Override.我认为这是一个偏好问题.

另请注意,还有Test :: LWP :: UserAgent,它为模拟用户代理的特定情况提供了许多不错的功能.我刚刚选择了LWP作为一个简单的例子.对于真正的代码,我更喜欢Test :: LWP :: UserAgent.

嘲笑其他东西

如果你需要处理一个数据库(比如MySQL),那么使用DBD :: sqlite和依赖注入来提供一个假的完整数据库,但真正的DBI是很好的.这甚至适用于DBIx :: Class.另一方面,如果数据库代码是您要测试的内容的一部分,要验证代码是否插入了正确的内容,您可以使用Test :: DatabaseRow.通常,只需查看CPAN上的Test :: namespace即可.那里有一些有趣的东西.您可以模拟time外部脚本URIURI,或甚至将所有测试包装在单独的Moose类中,以便以一种很好的方式组织测试套件.

我建议看一下Ian Langworth的Perl Testing:A Developer's Notebook,它给出了相当广泛的介绍.另一个很好的资源是Ovid 在github上免费测试培训.


1)注意这是一种不好的懒惰!

  • @HåkonHægland是的,它非常有用.遗憾的是,对于在线提供的更复杂程序的现代测试,没有真正好的指南.这些都是非常小的东西,或者只是文档.也许有人应该写一篇blog.perl.org帖子......听起来我是志愿者......:D (3认同)
  • 哇,嘲笑似乎很有趣..感谢您的精彩介绍. (2认同)