Often, I have a test like this:

  subtest "do things with an api" => sub {
    my $result = $api_client->do_first_thing;

    is(
      $result->documents->first->title,
      "The Best Thing",
    );

    ...
  };

Sometimes, the result comes back with zero documents.  ->first throws an
exception and then my whole test program comes crashing down and it's
miserable.  For a while, I've been meaning to make it possible for some
exceptions to be recognized by my test programs as instructions to emit a
failure, stop this subtest, and move on.

It's important to note that I'm talking, in the code above, about an exception
that would be thrown by ($result->documents) when ->first is called on it.
This kind of flow control eliminates needing to write:

  my $result = $api_client->do_first_thing;
  my $docs   = $result->documents;
  fail("no docs"), return unless $docs->has_entries;
  my $first  = $docs->first;
  fail("no title"), return unless $first->has_title;

  is(...);

In my case, I'm working with an API client that's specifically designed to be
used for testing, so this kind of loose coupling between thrown exceptions and
the test code is a good fit.  Not every exception should be caught this way.
Truly unexpected ones should still die.

So, I've written something to do this, and I'm sharing it here before going
further with it.  In my code, if an exception is meant to be caught and used as
a local abort instruction, it has a method called as_test_abort_events.  This
method returns a reference to an array of Test2 event descriptions.  For
example, maybe:

  sub as_test_abort_events {
    return [
      [ Ok   => (pass => 0, name => "no documents, but ->first called") ],
      [ Diag => (message => "collection state: ....") ],
    ];
  }

This method is easy to add to any exception you want, and you don't need to
change your exception class hierarchy in any way.

Next, you need something to run the tests and look for these exceptions being
thrown.  I have written a library for this, called Test::Abortable.

  https://github.com/rjbs/Test-Abortable

Test::Abortable provides two subroutines: subtest and testeval.  subtest acts
just like Test::More's subtest, but catches abort exceptions, emits their
events, and returns normally.  (I've also updated my library Test::Routine, in
a branch, to behave this way, as I use it in place of subtest for many things.)

testeval acts like eval, but only catches abort exceptions.

  ok(1);
  testeval {
    ok(2);
    this_throws_an_abort;
    ok(3);
  };
  ok(4);

This ends up emitting ok 1, ok 2, whatever the abort wants, and ok 4.  testeval
returns the return value of the code block if it succeeds.  If it fails due to
abort, it returns false, emits the abort events, and puts the abort in $@.  If
it fails because of any other exception, the exception is re-thrown.

Let me know if you have any thoughts before I begin using this in anger. :-)

-- 
rjbs

Attachment: signature.asc
Description: Digital signature

Reply via email to