问题
Suppose I have a class like this:
class MyClass {
method data-is-valid {
return self!get-data ~~ m{^From};
}
method !get-data {
return 'From Internet';
}
}
where !get-data
method gets some data from Internet.
Is it possible to mock that method so that it returns my own hardcoded data so I can test the module without connecting to the Internet?
Ideally, the solution should not modify the definition of the class in any way.
NOTE: A similar question exists regarding unittesting subroutines of modules.
回答1:
I would first refactor to pull the fetching logic out to a different object, and make MyClass
depend on it:
class Downloader {
method get-data {
return 'From Internet';
}
}
class MyClass {
has Downloader $.downloader .= new;
method data-is-valid {
return $!downloader.get-data ~~ m{^From};
}
}
This is an example of dependency inversion, which is a helpful technique for making code testable (and tends to make it easier to evolve in other ways too).
With this change, it is now possible to use the Test::Mock module to mock Downloader
:
use Test;
use Test::Mock;
subtest 'Is valid when contains From' => {
my $downloader = mocked Downloader, returning => {
get-data => 'From: blah'
};
my $test = MyClass.new(:$downloader);
ok $test.data-is-valid;
check-mock $downloader,
*.called('get-data', :1times);
}
subtest 'Is not valid when response does not contain From' => {
my $downloader = mocked Downloader, returning => {
get-data => 'To: blah'
};
my $test = MyClass.new(:$downloader);
nok $test.data-is-valid;
check-mock $downloader,
*.called('get-data', :1times);
}
回答2:
You probably want to take a look at Test::Mock. From its SYNOPSIS:
use Test;
use Test::Mock;
plan 2;
class Foo {
method lol() { 'rofl' }
method wtf() { 'oh ffs' }
}
my $x = mocked(Foo);
$x.lol();
$x.lol();
check-mock($x,
*.called('lol', times => 2),
*.never-called('wtf'),
);
回答3:
Hi @julio i suggest you take a look at the wrap function for routines, this should do what you need... https://docs.raku.org/language/functions#Routines ... this includes use soft; pragma to prevent inlining
回答4:
Probably the best idea would be to refactor the code (see Jonathan's answer)
However, If you cannot for some reason, there are still alternatives:
If the method is public you can simply create a subclass and override the method.
For example:
use Test;
class MyClass {
method data-is-valid {
return self.get-data ~~ m{^From};
}
method get-data {
return 'From Internet';
}
}
class MyClassTester is MyClass {
method get-data {
return 'Foobar';
}
}
my MyClassTester $class = MyClassTester.new;
nok $class.data-is-valid, 'Mocked class has invalid data';
done-testing;
If the method is private, you can use wrap
as stated on p6steve's answer. However you need introspection in order to modify the private method.
It can be done like this:
use Test;
class MyClass {
method data-is-valid {
return self!get-data ~~ m{^From};
}
method !get-data {
return 'From Internet';
}
}
my $class = MyClass.new;
my Method:D $get-data = $class.^find_private_method: 'get-data';
$get-data.wrap: { 'Foobar' };
nok $class.data-is-valid, 'Mocked class has invalid data';
done-testing;
来源:https://stackoverflow.com/questions/63042080/how-to-mock-a-class-method-when-unittesting-in-raku