1

Currently I am creating transactional tests like that:

use Test::More;
use Try::Tiny;

my $dbh = ...;

subtest 'do something envolving a transaction' => sub {
    $dbh->begin_work();
    try {
      my $obj = create_in_db({...}, $dbh);
      my $result = MyTestObject->new()->do_something($obj);
      ok $result "We've got great results";
    } catch {
        croak $_;
    } finally {
        $dbh->rollback(); #kills $obj
    };
};

done_testing();
1;

This works, but has the disadvantage, that the line of the error is always the catch block and the end of the subtest, never where the error actually happens. And it is a lot of boring boilerplate code that quickly adds up.

How to do this in a smarter way?

zdim
  • 64,580
  • 5
  • 52
  • 81
Jan
  • 6,532
  • 9
  • 37
  • 48

1 Answers1

2

The fact that the error (croak) is reported at the end of try-catch-finally blocks instead of where the offending code is called seems due to Try::Tiny's mixup with namespaces; see a discussion in this post and comments. The exact source of this misbehavior isn't clear to me in the complex try sub. A simple demo

use warnings;
use strict;
use feature 'say';

use Carp qw(croak);
use Try::Tiny;

sub this_croaks { croak "ouch from a sub in ", __PACKAGE__ }  # line 8

try {
    this_croaks();                    # line 11
}
catch   { print "In try: $_" }
finally { say "clean up" };           # line 14

This prints

In try: ouch from a sub in main at try_tiny_mixup.pl line 14.
clean up

But the croak-ing sub is called at line 11 so that should be reported, not line 14.

Changing croak to die makes it print line 8 (what of course isn't a solution) while using eval instead of Try::Tiny results in the correct line 11 printed (what is a valid solution). See the linked post. I am not aware of a fix with Try::Tiny but there are drop-in replacements, see below.

I don't see that this in any way depends on what tests are performed (here involving a database transaction as we are told). And I cannot check more specifically without a runable example.

The one thing that works fully is to revert to eval, which since 5.14 isn't anymore borne with subtleties that were the stated reason for Try::Tiny. Like

eval {
    this_croaks();
};
if ($@) { 
    print "In eval: $@";
}
say "clean up";

This is still archaic but it works just as intended (and Try::Tiny comes with twists of its own).

Hopefully the coming native try/catch, introduced as experimental in 5.34.0, won't have problems like this.§ For now it doesn't

use warnings;
use v5.34.0;

use Carp qw(croak);

use feature 'try';
no warnings qw(experimental::try);

sub this_croaks { croak "ouch from a sub in ", __PACKAGE__ }  # line 9

try {
    this_croaks();      # line 12
}
catch ($e)  { 
    print "In try: $e";
}
say "clean up";         # there is no "finally" keyword (see text and links)

This correctly pegs it as called at line 12 (and coming from line 9). Note that there is no finally keyword yet. The module Syntax::Keyword::Tiny (see footnote) does have it so it may be possible to use it as a drop-in replacement for Try::Tiny.

I suspect that clearing this up will clear up the test's behavior as well. (But I didn't get to test that.)


Syntax aids ("sugar") for anonymous subs (which in many ways aren't so naive)

Submitted a bug report

§ This is getting ported from Syntax::Keyword::Try by the author themselves so you may want to try that -- but then better use Feature::Compat::Try, they say. See docs of both, and see its tracker.

Once we are at experimental stuff see perlexperiment.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • The problem is, that in order for the test to function, it needs to throw in case of an unmet expectation. So just printing the error does not help. The `finally` functionality is important for the cleanup here, as it executes, both when the code fails and when not. Other than that, it would be `try { test } catch { cleanup; die $_; } cleanup;` (and I would get the 'wrong' line number again). – Jan Feb 23 '22 at 09:38
  • @Jan I don't understand? This is merely a demo to show that the problem (wrong line numbers shown for errors) is due entirely to `Try::Tiny`'s mixup. So I'd suggest to use `eval`, instead of `Try::Tiny`, with your real code -- if nothing else so to check. I can't reproduce your case in this answer and there is no fully runnable demonstration of the problem so I'm reduced to making stuff up. – zdim Feb 23 '22 at 18:22
  • @Jan As for the clean-up, do it (place a call to `cleanup()`?) _after_ `if ($@)...` it that's suitable. I edited the answer for that `finally`-cleanup to be reproduced correctly in other cases (good point, thank you), and with a few more tweaks – zdim Feb 23 '22 at 18:23
  • @Jan As stated in the answer, the `Syntax::Keyword::Try` (or `Feature::Compat::Try`) may be a full solution for you, as a mere drop-in replacement for `Try;;Tiny`. – zdim Feb 23 '22 at 18:28
  • @Jan Btw, if your DB-related `->do_something` throws (raises an exception) then no test happens; the processing is interrupted right there and the exception handled. This is possible I take it, being the reason why it is in the `try` block, right? – zdim Feb 23 '22 at 18:36
  • @Jan "_it needs to throw in case of an unmet expectation_" -- your sample code doesn't do that. The `ok` doesn't raise a `die`, it just prints. If you really want to throw on unmet expectation (why?) then you do that yourself and it's even simpler than in this post. I assumed that `try` protects against an exception thrown in processing, not one raised manually for testing purposes (what one normally doesn't do). – zdim Feb 23 '22 at 19:43