Test::Sweet Jonathan Rockway http://jrock.us/ ---- Test::Sweet? ---- Class-based testing But not xUnit ---- github://jrockway/test-sweet Also on CPAN (with docs) ---- Why Test::Sweet? ---- Excited by a coworker's Test::Class talk ---- But thought, "UGH, IT'S NOT 1985 ANYMORE" "MOOSE FIXES ALL THESE PROBLEMS" ---- This talk ---- Three parts ---- Maybe 2.5 parts ---- Why reuse? ---- How to use Test::Sweet; ---- How it's implemented ---- So you can help me hack it ---- Or so you can write something as cool ---- Why reuse ---- use Test::More; use My::App; ok My::App->return_true_if_it_works; done_testing; ---- So simple! ---- Why complicate it with all this crazy OO stuff!? ---- Yeah, why do that? ---- Remember CGI? ---- That didn't do that ---- Remember how much nicer things got when you started being sane? ---- DRY ---- More reliability ---- Doing things wrong is harder than doing them right ---- So you do them right ---- Let's see the syntax ---- class t::Foo { use Test::Sweet; test foo { is 2 + 2, 4, '2 + 2 is 4'; } } ---- Defines one test ---- Runnable with MooseX::Runnable ---- $ mx-run t::Foo ---- Normal Moose class ---- Normal Perl class ---- use t::Foo; use Test::More plan => 1; my $t = t::Foo->new; $t->foo; ---- Reusability ---- Roles ---- role t::Role::WithDatabase { use Test::Sweet; has 'dsn' => ( is => 'ro', isa => 'Str', required => 1, default => 'DBI:SQLite:foo', ); has 'dbh' => ( is => 'ro', lazy_build => 1, ); test connected_ok { is $self->dbh->status, 'CONNECTED'; } } ---- class t::Insert with t::Role::WithDatabase { use Test::Sweet; test basic_insert { ok $self->dbh->insert(1 => 'foo'); } test basic_select { is $self->dbh->select(1), 'foo'; } } ---- You now get three tests, and the expected reusability ---- What about per-file setup and teardown? ---- BUILD and DEMOLISH ---- One more thing ---- Test against default: $ mx-run t::Insert Test against someting else $ mx-run t::Insert --dsn 'DBI:Sybase:server=STAGING' ---- All attributes use MooseX::Getopt (If you have it.) ---- But wait, xUnit has a different setup/teardown ---- We have a more flexible mechanism ---- Test metaclass (with Traits) ---- How to use: ---- role Tmpdir { around run(@args){ my $tmp = ; $self->orig->(@args, $tmp); $tmp->cleanup; ok !-e $tmp, 'tmpdir went away ok'; } } ---- class FileTest { use Test::Sweet; test touch_file (+Tmpdir) { my $tmp = shift; ok !-e "$tmp/foo"; $tmp->touch('foo'); ok -e "$tmp/foo"; } test normal { ... } } ---- touch_file gets the tmpdir normal is normal ---- (Also, $test in the test is the test metaclass instance. So you can provide helper methods, have state, etc.) ---- What do we gain? ---- No more repetition ---- Compose in a role Use a trait ---- Adding another test doesn't require as much code ---- Real-world example ---- AnyEvent::Inotify::Simple ---- Lots of tests that: Create tmpdir Create directory watcher Create event loop Do things Check that events are fired ---- First implemented as cut-n-paste ---- Acceptable. Better than not testing ---- Then implemented as role that does this sequence, with a test method to verify stuff. ---- role TmpdirTest { has 'inotify'; has 'tmpdir'; requires 'actual_test'; test main { $self->tmpdir->setup; $self->actual_test; $self->tmpdir->cleanup; }; } ---- Too constrained. Very messy. One file per test case. ---- Spent more time writing the test runner than the actual module. ---- Traits are a comfortable middle ground. ---- test foo (WatchedDir) { ok $test->tmp->create('foo'); $test->watcher->poll; ok $test->tmp->create('bar'); is_deeply $test->watcher->poll, ['bar', 'create']; } ---- Summary: test keyword creates test method works like normal method, inside subtest control via metatest traits (setup/teardown) everything else is just Moose roles / method modifiers / attributes BUILD / DEMOLISH MooseX::Runnable MooseX::Getopt ---- OK, now tell me why you hate this ---- The bad: Not enough tests Not fast enough prove doesn't understand it yet ---- Bonus slides ---- How it works ---- Magic is Devel::Declare::Context::Simple ---- Extend it to parse: thing name (signature) : attributes { code goes here } ---- sub parser { ---- $self->init(@_); $self->skip_declarator; my $name = $self->strip_name; my $raw_proto = $self->strip_proto || ''; my $attrs = $self->strip_attrs || ''; my $requested_traits = $self->parse_proto($raw_proto . ' '. $attrs); ---- my $inject = $self->scope_injector_call(); $self->inject_if_block( $inject. ' my $self = shift; my $test = shift; ' ); ---- my $pack = Devel::Declare::get_curstash_name; Devel::Declare::shadow_sub("${pack}::test", sub (&) { my $test_method = shift; $pack->meta->add_test( $name, $test_method, $requested_traits ); }); ---- Installs magic Calls __PACKAGE__->meta->add_test when it sees a test ---- add_test is from the class meta-role (also adds get_all_tests) ---- add_test constructs metamethod object which wraps the test code with The Necessary Non-Magic Magic ---- metamethod object supplies new "body" which: starts a subtest creates new metatest instance (traits + your code) checks for errors ends subtest ---- metatest object simply encapsulates your test code with any extra traits ---- Call run to run ---- Run a test: $t->foo ---- Is actually: Test->meta->get_method('foo')->body->($t); ---- Which does: my $metatest = Moose::Meta::Class->create_anon_class( superclasses => 'Test::Sweet::Meta::Test', roles => cache => 1, ); my $m = $metatest->name->new( test_body => $metamethod->original_body ); $m->run; undef $m; Inside a subtest ---- Subtest allows: use Test::More tests => 1; my $t = Test->new; $t->a_test; ---- 1..1 ok 1 - a_test is doing something ok 2 - and another thing 1..2 ok 1 - Test::a_test ---- Last piece; run a "Suite" class Metarole applies Test::Sweet::Runnable role ---- my @tests = $self->meta->get_all_tests; plan tests => scalar @tests; # for progress bar $self->$_ for @tests; ---- This is composable further: my @classes = ...; for my $c (@classes) { subtest { $c->new->run } } ---- Tying everything together, Test::Sweet module ---- Moose::Util::MetaRole applies roles to metaclasses, imports sugar ---- Clone from github, improve, ask for commit bit!