warming up to modular testing (yapc::na::2009)

Download Warming up to Modular Testing (YAPC::NA::2009)

If you can't read please download the document

Upload: bradoaks

Post on 11-Jun-2015

994 views

Category:

Technology


2 download

DESCRIPTION

This talk will help you get started arranging your tests into modules. We'll cover setting up a simple TAP::Harness to run your tests. Then we'll see how Plus Three has used Test::Class to divide up and reuse code in our test suite.Separating tests out from a large .t file into modules and subroutines has helped me confirm more quickly that a code change has not introduced a regression. Developers save time by only running the relevant subset of tests before committing a change to the code or a change to the tests themselves.I'll offer a few tips on checking preconditions in your testing environment (e.g. is a daemon running, is an external service url reachable) andeither bailing out gracefully or trying to remedy the situation.You can ease into this modularization adventure. With Test::Class your shinier new tests can work right beside the venerable dustier ones letting you rework them as they need it.

TRANSCRIPT

  • 1. Warming up to modular testing
      • YAPC::NA::2009 June 23rd
    • Brad Oaks of Plus Three, LP

2. Who is this guy

  • First time YAPC speaker.
  • Not an expert on the subject.
  • Learned from an existing code base.

3. What we'll cover

  • The parts you'll need to move from a t/ directory full of flat .t files to a more modular setup.
  • A simple harness
  • An example of putting helper methods in their own module for reuse.
  • How Test::Class and friends ease inheritance in your tests to parallel inheritance in your main code.

4. The Harness 5. Harness to run the show

  • Runs the tests
  • Collects TAP formatted output from the tests.
  • We'll see a simplified example.

6. ./bin/micro_test

  • #!/usr/bin/env perl use strict; use warnings; use TAP::Harness; my @files = glob("t/pod.t");
  • my $harness = TAP::Harness->new();
  • $harness->runtests(@files);

7. ./bin/micro_test

  • #!/usr/bin/env perl use strict; use warnings; use TAP::Harness; my @files = glob("t/pod.t");
  • my $harness = TAP::Harness->new(
  • {verbosity => 1, merge => 0}
  • ); $harness->runtests(@files);

8.

  • BEGIN {
  • # Find a ARCOS_ROOT based on path to harness
  • my @dir = splitdir(canonpath($RealBin));
  • $ENV{ARCOS_ROOT} ||=
  • catdir(@dir[0 .. $#dir - 1]);
  • }

9.

  • BEGIN {
  • # Find a ARCOS_ROOT based on path to harness
  • my @dir = splitdir(canonpath($RealBin));
  • $ENV{ARCOS_ROOT} ||=
  • catdir(@dir[0 .. $#dir - 1]);
  • # use $ARCOS_ROOT/lib for modules
  • my $lib = catdir($ENV{ARCOS_ROOT}, "lib");
  • $ENV{PERL5LIB} =
  • $ENV{PERL5LIB}
  • ? "$ENV{PERL5LIB}:${lib}"
  • : "${lib}";
  • unshift @INC, $lib, "$lib/" . $Config{archname};
  • }

10.

  • BEGIN {
  • # Find a ARCOS_ROOT based on path to harness
  • my @dir = splitdir(canonpath($RealBin));
  • $ENV{ARCOS_ROOT} ||=
  • catdir(@dir[0 .. $#dir - 1]);
  • # use $ARCOS_ROOT/lib for modules
  • my $lib = catdir($ENV{ARCOS_ROOT}, "lib");
  • $ENV{PERL5LIB} =
  • $ENV{PERL5LIB}
  • ? "$ENV{PERL5LIB}:${lib}"
  • : "${lib}";
  • unshift @INC, $lib, "$lib/" . $Config{archname};
  • eval { require Arcos };
  • die cannot find Arcos if $@
  • }

11.

  • BEGIN {
  • # choose the first instance if not set.
  • unless ($ENV{ARCOS_INSTANCE}) {
  • require Arcos::Conf;
  • $ENV{ARCOS_INSTANCE} =
  • (Arcos::Conf->instances())[0];
  • }
  • }

12.

  • BEGIN {
  • # choose the first instance if not set.
  • unless ($ENV{ARCOS_INSTANCE}) {
  • require Arcos::Conf;
  • $ENV{ARCOS_INSTANCE} =
  • (Arcos::Conf->instances())[0];
  • }
  • }
  • use Arcos;
  • use Arcos::Script;

13.

  • use Arcos;
  • use Arcos::Script;
  • our ($help, $man, $archive);
  • my $v = 0;
  • my $files;
  • GetOptions('help'=> $help,
  • 'man'=> $man,
  • 'verbose'=> $v,
  • 'make_verbose=i'=> $v,
  • 'files=s'=> $files,
  • 'archive'=> $archive,
  • );

14. TAP::Harness can

    • try to emit results with color
    • be verbose or varying levels of quiet
    • run multiple test jobs at once (aggregate_tests)
  • see "perldoc TAP::Harness"
  • Test::Harness is for backwards compatibility.
  • For new work, you should be starting with TAP::Harness.

15. Common test functions 16.

  • Pull commonly used functions out into their own module.
  • Don't need inheritance for this.
  • contains:
  • Convenience methods.
  • Affecting the environment.
  • Checking the environment.

Arcos::Test 17.

  • can_reach_url() - uses LWP::UserAgent and checks response.
  • js_escape() - when searching for strings in HTML that has already been escaped.
  • get_server_uri() - composes url from host and port in Arcos::Conf

Arcos::TestConvenience Methods 18.

  • start/stop daemons
    • apache
    • Arcos queue
    • test SMTP server
  • These are basically system() calls to ctl scripts

Arcos::TestAffecting the Environment 19.

  • spread_is_running() -- used in t/island_mode.t
  • SKIP: {
  • skip('Spread is not running', 16) unless
  • Arcos::Test->spread_is_running();
  • # 16 tests is, isa, ok, like, etc.
  • }

Arcos::TestChecking the Environment 20.

  • spread_is_running() -- used in t/island_mode.t
  • SKIP: {
  • skip('Spread is not running', 16) unless
  • Arcos::Test->spread_is_running();
  • my $island_pp = catfile(ArcosRoot, 'bin',
  • 'arcos_island_postprocessor');
  • my $result = `$island_pp 2>&1`;
  • like($result, qr/Island Mode Post-Processing Complete/);
  • ok($result =~ /Submitted job (d+) : (S+) from (S+)./);
  • my ($job_id, $amount, $email) = ($1,$2,$3);
  • . . .
  • }

Arcos::TestChecking the Environment 21.

  • apache_is_running() -- used in t/open_handler.t
  • BEGIN {
  • if (Arcos::Test->apache_is_running) {
  • Test::More::plan('no_plan');
  • } else {
  • Test::More::plan(skip_all => "Arcos Apache is not running skipping all tests.");
  • }
  • }

Arcos::TestChecking the Environment 22. Test::Class 23.

  • The methods with Test attribute will be run.

Test::ClassMethod AttributesTest(4) 24.

  • The methods with Test attribute will be run.
  • The Test attribute specifies the number of tests for this method.

Test::ClassMethod AttributesTest(4) 25.

  • The methods with Test attribute will be run.
  • The Test attribute specifies the number of tests for this method.
  • The order they're run is determinedalphabetically by name.

Test::ClassMethod AttributesTest(4) 26.

  • The methods with Test attribute will be run.
  • The Test attribute specifies the number of tests for this method.
  • The order they're run is determinedalphabetically by name.
  • sub b_register : Test(15) { . . . }
  • sub d_short_pass : Test(4) { . . . }
  • sub e_mismatched_pass : Test(4) { . . . }
  • sub f_invalid_email : Test(4) { . . . }
  • sub h_duplicate_email : Test(4) { . . . }
  • sub h_duplicate_username : Test(4) { . . . }

Test::ClassMethod AttributesTest(4) 27.

  • The methods with Test attribute will be run.
  • The Test attribute specifies the number of tests for this method.
  • The order they're run is determinedalphabetically by name.
  • sub b_register : Test(15) { . . . }
  • sub d_short_pass : Test(4) { . . . }
  • sub e_mismatched_pass : Test(4) { . . . }
  • sub f_invalid_email : Test(4) { . . . }
  • sub h_duplicate_email : Test(4) { . . . }
  • sub h_duplicate_username : Test(4) { . . . }
  • sub b_submit_empty_form : Test(no_plan) { . . . }
  • You can specify no_plan for a specific sub.
  • Can localize the uncertainty instead of using no_plan for the entire module.

Test::ClassMethod AttributesTest(no_plan) 28.

  • startup / shutdown
    • run before /after the whole test class

Test::ClassMethod AttributesTest(startup) 29.

  • startup / shutdown
    • run before /after the whole test class
  • setup / teardown
    • run before / after every test method

Test::ClassMethod AttributesTest(startup) 30.

  • startup / shutdown
    • run before /after the whole test class
  • setup / teardown
    • run before / after every test method
  • sub tc_startup : Test(startup) { my $self = shift; $self->{td} = Arcos::TestData->new;
  • }

Test::ClassMethod AttributesTest(startup) 31.

  • startup / shutdown
    • run before /after the whole test class
  • setup / teardown
    • run before / after every test method
  • sub tc_startup : Test(startup) { my $self = shift; $self->{td} = Arcos::TestData->new;
  • }
  • sub tc_shutdown : Test(shutdown) { my $self = shift; $self->{td}->cleanup(ignore_deleted => 1);
  • }

Test::ClassMethod AttributesTest(startup) 32.

  • startup can have tests that count towards the plan
  • sub start : Test(startup => 1) { my $self = shift;
  • $self->{mail_tmp_dir} =
  • Arcos::Test->start_test_smtp_server;
  • use_ok('Arcos::JobQueue::Handler::CommunityInvitation');
  • }

Test::ClassMethod AttributesTest(startup) 33.

  • startup can have tests that count towards the plan.
  • sub start : Test(startup => 1) { my $self = shift;
  • $self->{mail_tmp_dir} =
  • Arcos::Test->start_test_smtp_server;
  • use_ok('Arcos::JobQueue::Handler::CommunityInvitation');
  • }
  • startup can also be specified on more than one method.

Test::ClassMethod AttributesTest(startup) 34. Inheritance in your tests 35.

  • A parent class for many of our tests.

Arcos::Test::Class 36.

  • Inherit from Test::Class
  • use base 'Test::Class';

Arcos::Test::Class 37.

  • Pull in some helper modules.
  • use base 'Test::Class'; use Arcos::Test; use Arcos::TestData; use Test::Builder qw(ok is_eq); use Test::LongString;

Arcos::Test::Class 38.

  • Store an Arcos::TestData reference.
  • use base 'Test::Class'; use Arcos::Test; use Arcos::TestData; use Test::Builder qw(ok is_eq); use Test::LongString;
  • sub tc_startup : Test(startup) { my $self = shift; $self->{td} = Arcos::TestData->new;
  • }

Arcos::Test::Class 39.

  • Call cleanup method.
  • use base 'Test::Class'; use Arcos::Test; use Arcos::TestData; use Test::Builder qw(ok is_eq); use Test::LongString;
  • sub tc_startup : Test(startup) { my $self = shift; $self->{td} = Arcos::TestData->new;
  • } sub tc_shutdown : Test(shutdown) { my $self = shift; $self->{td}->cleanup(ignore_deleted => 1);
  • }

Arcos::Test::Class 40. Arcos::Test::Class

  • We also provide convenience methods at this level:
    • contains_url()
    • lacks_url()
    • json_contains()
    • json_lacks()
    • json_success()
    • json_failure()
    • input_value_is()

41. Arcos::Test::Class

  • help Test::Builder give the right line number for failures
    • sub json_success {
    • my $self = shift;
    • {
    • local $Test::Builder::Level =
    • $Test::Builder::Level + 1;
    • $self->json_contains('success');
    • }
    • }
  • We want the error to show up where json_success() is called, not way down here in the helper module.

42. Inherited Forms, with Inherited Tests

  • Arcos::Form::Test is the parent of
    • Arcos::Form:: ContactOfficial ::Test
    • Arcos::Form:: Register ::Test
  • They each have
  • use base 'Arcos::Form::Test';

43. Arcos::Form::Test the parent

  • Startup and Shutdown
  • sub tc_startup : Test(startup) { . . . } sub tc_shutdown : Test(shutdown) { . . . } sub x_create_form : Test(startup => 3 ){ . . . }
  • Intended to be overridden
  • sub additional_elements { '' } sub form_db_class { '' }
  • Shared methods for these forms sub mech_shows_custom_fields { . . . }
  • sub person_has_custom_fields { . . . }

44. An example of inherited tests

  • tc_startup can be completely overridden
  • or augment by calling SUPER
  • sub tc_startup : Test(startup) {
  • $self->{td} = Arcos::TestData->new();
  • return $self->SUPER::tc_startup;
  • }

45. Inheritance Example 46. We still have .t filest/register_form.t

  • use strict; use warnings; use Arcos::Test; use Arcos::Test::Script; use Arcos::Form::Register::Test; Arcos::Test->needs_running_krang(); Arcos::Test->needs_running_apache(); Arcos::Form::Register::Test->new->runtests;

47. We still have .t filest/contact_official_form.t

  • ContactOfficial forms need a little more at startup.
  • use Arcos::GeoCoder; my $coder = Arcos::GeoCoder->new; if (!$coder->available) { Test::More::plan(skip_all =>
  • 'geocoding databases not available.');
  • exit;
  • }

48. We still have .t filest/contact_official_form.t

  • ContactOfficial forms need a little more at startup.
  • use Arcos::GeoCoder; my $coder = Arcos::GeoCoder->new; if (!$coder->available) { Test::More::plan(skip_all =>
  • 'geocoding databases not available.');
  • exit;
  • } unless (Arcos::Test->stop_queue()) { Test::More::plan(skip_all =>
  • 'Could not stop Queue daemon in time.');
  • exit;
  • }

49. We still have .t filest/contact_official_form.t

  • ContactOfficial forms need a little more at startup.
  • use Arcos::GeoCoder; my $coder = Arcos::GeoCoder->new; if (!$coder->available) { Test::More::plan(skip_all =>
  • 'geocoding databases not available.');
  • exit;
  • } unless (Arcos::Test->stop_queue()) { Test::More::plan(skip_all =>
  • 'Could not stop Queue daemon in time.');
  • exit;
  • } Arcos::Form::ContactOfficial::Test->new->runtests;

50. We still have .t filest/contact_official_form.t

  • ContactOfficial forms need a little more more at startup.
  • use Arcos::GeoCoder; my $coder = Arcos::GeoCoder->new; if (!$coder->available) { Test::More::plan(skip_all =>
  • 'geocoding databases not available.');
  • exit;
  • } unless (Arcos::Test->stop_queue()) { Test::More::plan(skip_all =>
  • 'Could not stop Queue daemon in time.');
  • exit;
  • } Arcos::Form::ContactOfficial::Test->new->runtests;
  • Arcos::Test->restart_queue;

51. return to skip

  • Section of a method not yet applicable
  • return 'The default templates do not currently have security code; skipping for now.';
  • Whole method not yet implemented
  • return 'The deleted form test is not implemented for this form type yet.';
  • Fail safe check
  • return 'You have no test payment account.'
  • unless $self->test_account;

52. plan to skip

  • use Arcos::Conf qw(ContributionTestMode);
  • if (!ContributionTestMode) {
  • plan(skip_all => 'contributions not in test mode');
  • } elsif (!Arcos::Test->can_reach_url(
  • 'https://www.vancodev.com:443/cgi-bin/wstest.vps')) {
  • plan(skip_all => "Can't reach https://www.vancodev.com:443/cgi-bin/wstest.vps");
  • } else { Arcos::ContributionForm::Test::Vanco->runtests(); }

53. TEST_METHOD

  • TEST_METHOD=a_good_file
  • bin/arcos_test --files=t/admin-listupload.t
  • TEST_METHOD=e_failed_contributions
  • bin/arcos_test --files=t/report-contribution.t
  • TEST_METHOD=k_listing_checkbox
  • bin/arcos_test --files=t/admin-mailingmessage.t

54. TEST_METHOD

  • TEST_METHOD=a_good_file
  • bin/arcos_test --files=t/admin-listupload.t
  • TEST_METHOD=e_failed_contributions
  • bin/arcos_test --files=t/report-contribution.t
  • TEST_METHOD=k_listing_checkbox
  • bin/arcos_test --files=t/admin-mailingmessage.t
  • TEST_METHOD='(a|b)_.*'
  • bin/arcos_test --files=t/warehouse-loader.t

55. a lot of reuse

  • use base 'Arcos::Report::SiteEvent::Test';
  • Arcos::Report::Volunteer::Test
  • Arcos::Report::Contribution::Test
  • Arcos::Report::ContactOfficial::Test
  • Arcos::Report::Petition::Test
  • Arcos::Report::Tellafriend::Test
  • Arcos::Report::Subscription::Test
  • Arcos::Report::RadioCall::Test
  • Arcos::Report::LTE::Test
  • Arcos::Report::ContactUs::Test

56. Bonus slides 57. TODO tests

  • sub live_test : Test{
  • local $TODO =
  • "live currently unimplemented";
  • ok(Object->live, "object live");
  • };
  • Skip is so darn easy we haven't used TODO like this.

58. pausing to look around

  • warn 'pausing to look around;
  • hit return to continue:';
  • my $PAUSE = ;
  • This will keep the test from cleaning up before you've had a chance to look around.
  • Add a few warns of URL values and you can bring up the reports in a browser to get a better idea of what's going on.
  • Litter this within a specific method, or in your method with the Test(shutdown) attribute.

59. calling plan from Arcos::Test

  • I added aneeds_run_dailymethod to bail out gracefully.
  • if ($ENV{'RUN_DAILY_TESTS'}) { if (
  • $ENV{'OVERRIDE_DAILY_TESTS'}
  • or $class->_needs_daily_run()
  • ) { $class->_update_run_daily_timestamp(); Test::More::plan(@args); }
  • } else { Test::More::plan( skip_all => 'RUN_DAILY_TESTS environment variable not set; skipping daily tests.'); }

60. Additional Resources 61. Additional Resources

  • #perl-qa on the irc.perl.org server
  • http://search.cpan.org/~adie/Test-Class-0.31/lib/Test/Class.pm
  • http://search.cpan.org/~mschwern/Test-Simple-0.88/lib/Test/Builder.pm
  • Organizing Test Suites with Test::Class
  • http://www.modernperlbooks.com/mt/2009/03/organizing-test-suites-with-testclass.html

62. Thank you for your time!