Beefy Boxes and Bandwidth Generously Provided by pair Networks
Just another Perl shrine
 
PerlMonks  

Testing with Test::Mock::HTTP::Tiny

by Bod (Parson)
on Sep 26, 2023 at 20:11 UTC ( [id://11154671] : perlquestion . print w/replies, xml ) Need Help??

Bod has asked for the wisdom of the Perl Monks concerning the following question:

I am writing some tests of a module that fetches a webpage using HTTP::Tiny->get

To test it, I am trying to use Test::Mock::HTTP::Tiny but I've never tried to use a Test::Mock module before. It's been on my radar since kcott mentioned their existence many moons ago. Now I have the need for one...but the documentation is lacking (to put it mildly!)

First I've run this code to get the mock data:

use strict; use warnings; use HTTP::Tiny; use Test::Mock::HTTP::Tiny; my $http = HTTP::Tiny->new; my $resp = $http->get('http://www.way-finder.uk/'); open my $fh, '>', 'mock_html.dat'; print $fh Test::Mock::HTTP::Tiny->captured_data_dump; close $fh;
Then I've renamed all the references to the domain www.way-finder.uk (a real domain) to www.testing.crawl (a mock domain). I've done this because I don't want the tests going out to a live site as it will change over time invalidating the tests.

My test file looks like this:

use strict; use warnings; use Test::More; use Test::Mock::HTTP::Tiny; use WWW::Crawl; plan tests => 1; $/ = undef; open my $fh, '<', 't/mock_html.dat' or die "Can't open datafile"; my $replay = <$fh>; close $fh; die "Nothing to replay" unless $replay; Test::Mock::HTTP::Tiny->set_mocked_data($replay); my $crawl = WWW::Crawl->new( 'timestamp' => 'a', ); my @links = $crawl->crawl('https://www.testing.crawl', \&link); cmp_ok ( scalar @links, '==', 8, 'Correct link count'); sub link { diag ($_[0]); }
The method $crawl->crawl uses HTTP::Tiny->get to get the web address.

My expectation was that Test::Mock::HTTP::Tiny would replay the website to HTTP::Tiny but instead it gives the HTTP error 599 - Internal Exception

Is this the right way to use Test::Mock objects or am I completely off track here?

EDIT:
Corrected typo in title

ANOTHER EDIT:
Corrected module in title!

Replies are listed 'Best First'.
Re: Testing with Test::Mock::HTTP::Tiny
by bliako (Monsignor) on Sep 27, 2023 at 08:06 UTC

    Test::Mock::HTTP::Tiny's set_mocked_data() expects an arrayref or a hashref. You are providing it with a scalar (string).

    The captured data is indeed an arrayref as Test::Mock::HTTP::Tiny->captured_data_dump shows. But what you save to a file is a text representation of a Perl data structure. What you get when you read that data back from file is a scalar string. And that's what you pass to Test::Mock::HTTP::Tiny's set_mocked_data() which silently ignores you!

    You need to "undump" the file contents somehow and resurrect the Perl data structure you started with. The simplest way to achieve this is by eval()'ing the text representation minus the $VAR1 = at the beginning of the string (placed there by Data::Dumper) (see the warnings at the end of this post):

    $replay =~ s/^\$VAR1\s*=\s*//; $replay = eval $replay; print "ref: ".ref($replay)."\n"; Test::Mock::HTTP::Tiny->set_mocked_data($replay);

    You were unlucky in debugging -- in the source code of Test::Mock::HTTP::Tiny I see this:

    if (ref($new_mocked_data) eq 'ARRAY') { ... elsif (ref($new_mocked_data) eq 'HASH') { ... else { # TODO: error }

    Apropos HTTP mocking, https://blogs.perl.org/users/e_choroba/2016/01/post.html by choroba presents an alternative.

    WARNING: Be warned that eval()'ing things from files is a huge security hole. An alternative would be to use Storable to dump/freeze and resurrect/thaw a Perl data structure. But it too has boldface security warnings:

    my $mocked_data = Test::Mock::HTTP::Tiny->mocked_data(); store $mocked_data, 'mock_html.dat'; ... my $replay = retrieve 'mock_html.dat'; print "ref: ".ref($replay)."\n"; Test::Mock::HTTP::Tiny->set_mocked_data($replay);

    bw, bliako

      You need to "undump" the file contents somehow and resurrect the Perl data structure you started with. The simplest way to achieve this is by eval()'ing the text representation minus the $VAR1 = at the beginning of the string (placed there by Data::Dumper) (see the warnings at the end of this post):

      Shameless plug: For many data structures, it's possible to do this more safely via my Data::Undump::PPI (see the disclaimer in Config::Perl).

      However, looking at the source, it would seem the simpler solution is to just serialize Test::Mock::HTTP::Tiny->captured_data oneself in a more suitable format, perhaps JSON or YAML (AFAICT from looking at the source, the data structure doesn't look like it contains Perl objects that would pervent serialization, though at the moment I don't have the time to test).

        it would seem the simpler solution is to just serialize ...

        yep, good point haukex++ as this is the easiest and most secure by far. Since I have more time:

        use strict; use warnings; use HTTP::Tiny; use Test::Mock::HTTP::Tiny; use JSON; my $http = HTTP::Tiny->new; my $resp = $http->get('http://www.way-finder.uk/'); # EDIT: it's captured_data() not mocked_data() see below #my $json_str = eval { JSON::encode_json(Test::Mock::HTTP::Tiny->mocke +d_data()) }; my $json_str = eval { JSON::encode_json(Test::Mock::HTTP::Tiny->captur +ed_data()) }; die "failed to encode json" if $@; open my $fh, '>', 'mock_html.dat'; print $fh $json_str; close $fh;

        and

        use strict; use warnings; use Test::More; use Test::Mock::HTTP::Tiny; use WWW::Crawl; use JSON; plan tests => 1; $/ = undef; open my $fh, '<', 't/mock_html.dat' or die "Can't open datafile"; my $replay = <$fh>; close $fh; $replay = eval { JSON::decode_json($replay) }; ok(!$@, "parsed JSON replay data") or BAIL_OUT($@); is(ref($replay), 'ARRAY', "parsed JSON replay data is an ARRAY"); die "Nothing to replay" unless $replay; Test::Mock::HTTP::Tiny->set_mocked_data($replay); my $crawl = WWW::Crawl->new( 'timestamp' => 'a', ); my @links = $crawl->crawl('https://www.testing.crawl', \&link); cmp_ok ( scalar @links, '==', 8, 'Correct link count'); sub link { diag ($_[0]); }

        note: eval() around JSON subs is because it dies on error last time I checked.

        bw, bliako

      You were unlucky in debugging

      * kicks self *

      I looked at the source code and didn't pick that up...I must have been tired or having a senior moment!

      But, it begs the question...
      Should I be using this module at all?

      • It hasn't been updated in 8 years
      • It's unlikely to be on the target machine
      • It's only used for testing
      • Its documentation is virtually non-existant
      • It appears unfinished

      I try not to get users to install non-core modules unless really necessary. Especially for testing.

      Be warned that eval()'ing things from files is a huge security hole

      Is this perhaps a security hole worth accepting because it is only used in testing? So the input file is known and anyone changing the file would be potentially sabotaging themself.

      You need to "undump" the file contents somehow and resurrect the Perl data structure you started with

      This sounds like the perfect use for a JSON object, especially as JSON::PP is core...

Re: Testing with Test::Mock::Tiny::HTTP
by kcott (Archbishop) on Sep 27, 2023 at 11:18 UTC

    G'day Bod,

    Not an answer to your question but a heads-up regarding a security vulnerability with HTTP::Tiny: CVE-2023-31486.

    The module documentation discusses this. Two of the easiest fixes would be:

    # Change use HTTP::Tiny; # to use HTTP::Tiny 0.083;

    or

    # Change my $http = HTTP::Tiny->new; # to my $http = HTTP::Tiny->new(verify_SSL => 1);

    — Ken

      Thanks Ken,

      I've changed to use HTTP::Tiny 0.083; and also set this as the minimum version in Makefile.PL