Let’s assume for the moment that you’re writing a Perl module or application. You’d like to maintain some level of software quality (or kwalitee), so you’re writing a suite of test scripts. Whether you’re writing them first (good for you for practicing test-driven development!) or the application code is already there, you’ll probably be reaching for Test::Simple, Test::More, or one of the Test2::Suite bundles. With the latter two you’re immediately confronted with a choice: do you count up the number of tests into a plan, or do you forsake that in favor of leaving a done_testing() call at the end of your test script(s)?
There are good arguments for both approaches. When you first start, you probably have no idea how many tests your scripts will contain. After all, a test script can be a useful tool for designing a module’s interface by writing example code that will use it. Your exploratory code would be written as if the module or application was already done, testing it in the way you’d like it to work. Not declaring a plan makes perfect sense in this case; just put done_testing() at the end and get back to defining your tests.
You don’t have that option when using Test::Simple, of course—it’s so basic it only has one function (ok()), and you have to pre-declare how many tests you plan to run when useing the module, like so:
use Test::Simple tests => 23;
Test::More also supports this form of plan, or you can opt to use its plan function to state the number of tests in your script or subtest. With Test2 you have to use plan. Either way, the plan acts as a sort of meta-test, making sure that you executed exactly what you intended: no more, no less. While there are situations where it’s not possible to predict how many times a given set of tests should run, I would highly suggest that in all other cases you should “clean up” your tests and declare a plan. Later on, if you add or remove tests you’ll immediately be aware that something has changed and it’s time to tally up a new plan.
What about other Perl testing frameworks? They can use plans, too. Here are two examples:
Thoughts? Does declaring a test plan make writing tests too inflexible? Does not having a plan encourage bad behavior? Tell me what you think in the comments below.
9 thoughts on “Testing Perl: To plan or not to plan”
I always try to use a plan. Without it, some of the tests could get skipped without a notice. It’s happened to me and the pain of recalculating the plan on every test change is less. For more complex test files, I plan e.g. 10 + 12 + 1 with a comment explaining what each number means.
Tests need to be easy to write to encourage people to actually write them, any simplification in that direction desireable. Myself, I always develop tests without a plan these days because it makes it easier to add more of them on the fly.
I could see an argument to add a plan later in the process with the file is essentially “closed” and the count has become stable.
(I also used to have my dev tools– emacs perlnow.el– set-up to automatically revise the count. Personally, I quit using that feature, but hypothetically, that’d be another option to get an open count during development but a fixed (“frozen”?) one after shipping.)
I used to be very insistent on a plan, but as I’ve used modern Perl testing more I’ve found that the problem of skipped tests is less of one than I expected. At ZipRecruiter, the standard is to use done_testing(), and it seems to work pretty well.
You have no doorway from the kitchen to the formal dining room? That’s going to make meals awkward, won’t it. I would blow away the walls separating kitchen=>dining, entrance=>dining and kitchen=> that square space connected to the entrance, which seems to me to be a lot of square footage wasted.
🙂
Finally we’re sparking some quality discussion here.
The only things I watch on TV consistently are sports, HGTV, DIY, Food Network and Cooking Channel.
done_testing is the balanced way. Manual plan and no-plan are the opposite extremes,
done-testing will output the plan, which is formulated from how many tests ran. No-plan means anything goes, and a specified plan is a direct number.
Done-testing at the end of the test does insure the test completes, so no need to worry about early exits being missed. It is still possible to use conditionals and end up with tests unintentionally skipped, but in my experience most people set the plan by running without a plan, seeing how many ran, then setting the number, which will also miss the skipped tests. If you are not literally manually counting assertions in the test then setting a plan and done-testing are exactly the same, except setting the plan is more work.
In my experience I have only needed to set a plan when I have tests that fork or run threads, in those cases done-testing will not always catch things that are missed or an early exit from a child.
-Chad ‘Exodist’ Granum, maintainer of test-simple and test-more, author of Test2
Thanks for that insight! Maybe it would be a good idea to add such guidance to the documentation?
{"id":"11","mode":"button","open_style":"in_modal","currency_code":"USD","currency_symbol":"$","currency_type":"decimal","blank_flag_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":500,"top_media_type":"featured_image","featured_image_url":"https:\/\/phoenixtrap.com\/wp-content\/uploads\/2021\/02\/image-200x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Leave a tip!","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"The Phoenix Trap","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"The Phoenix Trap","form_subtitle":"Do you like what you see? Leave a one-time or recurring tip!","currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for being a supporter!","payment_confirmation_title":"The Phoenix Trap","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"What is the name on your credit card?"},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"What is your email address?"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
I always try to use a plan. Without it, some of the tests could get skipped without a notice. It’s happened to me and the pain of recalculating the plan on every test change is less. For more complex test files, I plan e.g. 10 + 12 + 1 with a comment explaining what each number means.
Tests need to be easy to write to encourage people to
actually write them, any simplification in that
direction desireable. Myself, I always develop tests
without a plan these days because it makes it easier to
add more of them on the fly.
I could see an argument to add a plan later in the
process with the file is essentially “closed” and the
count has become stable.
(I also used to have my dev tools– emacs perlnow.el–
set-up to automatically revise the count. Personally, I
quit using that feature, but hypothetically, that’d be
another option to get an open count during development
but a fixed (“frozen”?) one after shipping.)
I used to be very insistent on a plan, but as I’ve used modern Perl testing more I’ve found that the problem of skipped tests is less of one than I expected. At ZipRecruiter, the standard is to use done_testing(), and it seems to work pretty well.
You have no doorway from the kitchen to the formal dining room? That’s going to make meals awkward, won’t it. I would blow away the walls separating kitchen=>dining, entrance=>dining and kitchen=> that square space connected to the entrance, which seems to me to be a lot of square footage wasted.
🙂
Finally we’re sparking some quality discussion here.
The only things I watch on TV consistently are sports, HGTV, DIY, Food Network and Cooking Channel.
done_testing is the balanced way. Manual plan and no-plan are the opposite extremes,
done-testing will output the plan, which is formulated from how many tests ran. No-plan means anything goes, and a specified plan is a direct number.
Done-testing at the end of the test does insure the test completes, so no need to worry about early exits being missed. It is still possible to use conditionals and end up with tests unintentionally skipped, but in my experience most people set the plan by running without a plan, seeing how many ran, then setting the number, which will also miss the skipped tests. If you are not literally manually counting assertions in the test then setting a plan and done-testing are exactly the same, except setting the plan is more work.
In my experience I have only needed to set a plan when I have tests that fork or run threads, in those cases done-testing will not always catch things that are missed or an early exit from a child.
-Chad ‘Exodist’ Granum, maintainer of test-simple and test-more, author of Test2
Thanks for that insight! Maybe it would be a good idea to add such guidance to the documentation?
yeah, the docs could be improved. These are the closest 2, and they are sparse:
http://test-more.github.io/Test2-Suite/#tutorial-Planning-done_testing
https://metacpan.org/pod/Test2::Manual::Testing::Planning