標準出力や標準エラー出力を捕まえてテストする Test::Output / Capture::Tiny

Perl で、あるコードが標準エラー出力に吐き出した内容をテストしたい場面がありました。

自分でまず思いついたのは STDERR を dup して保存しておいて、ファイルにリダイレクトして、元に戻して、というやりかた。これはこれで動くのですが面倒。こういう場合は Test::Output (や miyagawa さんに教えてもらった Capture::Tiny) が便利です。

Test::Output はこんな感じ。 std(out|err)_(is|isnt|like) といったテスト関数が使えるようになります。

use Test::Output;
use Test::More;

stderr_is {
    # STDERR になにか出力するコード
} "STDERRの内容", "description";

stdout_like {
    # code
} qr/regexp/, "description";

Capture::Tiny を使うと

use Capture::Tiny qw/ capture /;
my ($stdout, $strerr) = capture {
    # code
};

このようにして出力を捕まえることができます。

Capture::Tiny の SEE ALSO には、「出力を捕まえるモジュールはCPANにたくさんあるけど、いまいち用途が限定的なので作ったよ」(意訳)というようなことが書いてありました。汎用的に STDOUT, STDERR を捕まえるのには Capture::Tiny が良さげですね。

[追記]
Capture::Tiny は子プロセスの出力も捕まえられますが、Test::Output ではそれはできないようです。

use strict;
use Capture::Tiny qw/ capture /;
use Test::More;
use Test::Output;

my %Tests = (
    internal   => sub { print "FOO" },
    subprocess => sub { system("echo", "-n", "FOO") },
);
my @Captures = (
    sub {
        my ($type, $code) = @_;
        my ($out) = capture { $code->() };
        is $out, "FOO", "Capture::Tiny $type";
    },
    sub {
        my ($type, $code) = @_;
        stdout_is { $code->() } "FOO", "Test::Output $type";
    },
    sub {
        my ($type, $code) = @_;
        my $cap;
        open my $out, ">", \$cap;
        local *STDOUT = $out;
        $code->();
        is $cap, "FOO", "PerlIO $type";
    },
);

for my $capture ( @Captures ) {
    for my $type ( keys %Tests ) {
        $capture->( $type, $Tests{$type} );
    }
}
done_testing;

Perl で print "FOO" するのと、system で echo -n FOO するのを、それぞれ Capture::Tiny, Test::Output, PerlIO で捕まえようとしてみます。
実行結果はこうなりました。

ok 1 - Capture::Tiny subprocess
ok 2 - Capture::Tiny internal
FOOnot ok 3 - Test::Output subprocess
#   Failed test 'Test::Output subprocess'
#   at cap.pl line 18.
# STDOUT is:
# 
# not:
# FOO
# as expected
ok 4 - Test::Output internal
FOOnot ok 5 - PerlIO subprocess
#   Failed test 'PerlIO subprocess'
#   at cap.pl line 26.
#          got: undef
#     expected: 'FOO'
ok 6 - PerlIO internal
1..6
# Looks like you failed 2 tests of 6.

Test::Output, PerlIO だと子プロセスの出力を捕まえることができずに、失敗しています。