开发者

Perl, strings, floats, unit testing and regexps!

开发者 https://www.devze.com 2023-02-09 08:25 出处:网络
OK, as a preface this question potentially is \'stupider\' than my normal level of question - however this problem has been annoying me for the last few days so I\'ll ask it anyway. I\'ll give a mock

OK, as a preface this question potentially is 'stupider' than my normal level of question - however this problem has been annoying me for the last few days so I'll ask it anyway. I'll give a mock example of what my problem is so I can hope to generalize it to my current problem.

#!/usr/bin/perl -w use strict;

use Test::More 'no_plan';

my $fruit_string = 'Apples cost $1.50';
my ($fruit, $price) = $fruit_string =~ /(\w+)s cost \$(\d+\.\d+)/;

# $price += 0; # Uncomment for Great Success
is ($price, 1.50, 'Great Success');

Now when this is run I get the message

#   Failed test 'Great Success'
#          got: '1.50'
#     expected: '1.5'

To make the test work - I either uncomment the commented line, or use is ($price, '1.50', 'Great Success'). Both options do not work for me - I'm tes开发者_如何学运维ting a huge amount of nested data using Test::Deep and cmp_deeply. My question is, how can you extract a double from a regexp then use it immediately as a double - or if there is a better way altogether let me know - and feel free to tell me to take up gardening or something lol, learning Perl is hard.


You're already using Test::Deep, so you can simply use the num() wrapper to perform a numerical rather than stringwise comparison (it even lets you add in a tolerance, for comparing two inexact floating point values):

cmp_deeply(
    $result,
    {
        foo         => 'foo',
        bar         => 'blah',
        quantity    => 3,
        price       => num(1.5),
    },
    'result hash is correct',
);

For normal comparisons done separately, cmp_ok will work, but num() is still available: cmp_deeply($value, num(1.5), 'test name') still works.


Force $price to be interpreted as a number:

is ( 0 + $price, 1.50, 'Great Success');


Why not use the tried-and-true ok? You'll be testing the thing that you actually mean to test, and not have to worry about whether is is doing anything too subtle or too clever.

ok($price == 1.5, 'Great Success');

is does provide some additional diagnostics on failure, but that is easy enough to do with ok, too

ok($price == 1.5, 'Great Success') or diag("Expected \$price==1.5, got $price");


Your tests are failing because is($x, $y, $name) is equivalent to cmp_ok($x, 'eq', $y, $name). The eq forces each of its arguments to be evaluated as strings. Since you want numeric equality, you could write it out with cmp_ok using '=='. You could make things easier by writing your own numeric version of is:

sub is_num {cmp_ok $_[0], '==', $_[1], $_[2]}

But that version is subtly broken, it will report errors on the wrong lines. To make sure that error reporting shows the right lines:

sub is_num {splice @_, 1, 0, '=='; goto &cmp_ok}

The reason for the goto &sub is because cmp_ok uses caller to determine where the error happened. The goto &sub syntax erases the call frame setup for is_num so that cmp_ok thinks its being called from the location that is_num was.

Lastly, a plug of my module Test::Magic which provides syntactic sugar for Test::More:

use Test::Magic 'no_plan';

... # setup code

test 'fruit price',
  is $price == 1.50;

Which is interpreted as cmp_ok( $price, '==', 1.50, 'fruit price')


The reason for this behavior is that is uses eq to do comparisons, which forces stringification of its arguments. 1.50 stringifies to '1.5' and this fails.

Your choices are to live with the behavior (force stringification or numification) or else write your own alternative to is which will compare numerically if both sides look like they are numbers before falling back on eq comparisons. I would personally go with the latter approach.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号