{"id":4459,"date":"2018-12-15T16:41:59","date_gmt":"2018-12-15T16:41:59","guid":{"rendered":"https:\/\/www.blopig.com\/blog\/?p=4459"},"modified":"2018-12-15T16:42:48","modified_gmt":"2018-12-15T16:42:48","slug":"automated-testing-with-doctest","status":"publish","type":"post","link":"https:\/\/www.blopig.com\/blog\/2018\/12\/automated-testing-with-doctest\/","title":{"rendered":"Automated testing with doctest"},"content":{"rendered":"<p>One of the ways to make your code more robust to unexpected input is to develop with boundary cases in your mind. Test-driven code development begins with writing a set of unit tests for each class. These tests often includes normal and extreme use cases. Thanks to packages like <code>doctest<\/code> for Python, Mocha and Jasmine for Javascript etc., we can write and test codes with an easy format. In this blog post, I will present a short example of how to get started with doctest in Python. N.B. <code>doctest<\/code> is best suited for small tests with a few scripts. If you would like to run a system testing, look for some other packages!<\/p>\n<p><!--more--><\/p>\n<p>Assume you have a small function that turns a set of input into an array of 3D coordinates:<\/p>\n<p><code><\/p>\n<pre>\r\nimport numpy as np\r\ndef Point3D(*args):\r\n    return np.array(args,dtype=np.float64)\r\n<\/pre>\n<p><\/code><\/p>\n<p>If you&#8217;d like to ensure that only three numbers are fed into the input:<\/p>\n<p><code><\/p>\n<pre>\r\ndef Point3D(*args):\r\n    if len(args) != 3:\r\n        print('Not a point in 3D')\r\n        return;\r\n    else:\r\n        return np.array(args,dtype=np.float64)\r\n<\/pre>\n<p><\/code><\/p>\n<p>Even more, if you catch a non-floating point input:<\/p>\n<p><code><\/p>\n<pre>\r\ndef Point3D(*args):\r\n    if len(args) != 3:\r\n        print('Not a point in 3D')\r\n        return;\r\n    else:\r\n        try:\r\n            return np.array(args,dtype=np.float64)\r\n        except:\r\n            print('Cannot be converted to float')\r\n<\/pre>\n<p><\/code><\/p>\n<p>To incorporate some testing to see that you&#8217;re catching it correctly, I have the following three test cases:<\/p>\n<table>\n<tr>\n<th>Input<\/th>\n<th>Expected output<\/th>\n<\/tr>\n<tr>\n<td>Point3D(0,0,0)<\/td>\n<td>array([0., 0., 0.])<\/td>\n<\/tr>\n<tr>\n<td>Point3D(0,2)<\/td>\n<td>Not a point in 3D<\/td>\n<\/tr>\n<tr>\n<td>Point3D(0,&#8217;A&#8217;,0)<\/td>\n<td>Cannot be converted to float<\/td>\n<\/tr>\n<\/table>\n<p><code>doctest<\/code> reads the multiline string between the function definition and the first line of the function. You preceed the input with <code>&gt;&gt;&gt;<\/code> and the output in the subsequent line:<\/p>\n<p><code><\/p>\n<pre>\r\nimport numpy as np\r\ndef Point3D(*args):\r\n    \"\"\"\r\n    &gt;&gt;&gt; Point3D(0,0,0)\r\n    array([0., 0., 0.])\r\n    &gt;&gt;&gt; Point3D(0,2)\r\n    Not a point in 3D\r\n    &gt;&gt;&gt; Point3D(0,'A',0)\r\n    Cannot be converted to float\r\n    \"\"\"\r\n    if len(args) != 3:\r\n        print('Not a point in 3D')\r\n        return;\r\n    else:\r\n        try:\r\n            return np.array(args,dtype=np.float64)\r\n        except:\r\n            print('Cannot be converted to float')\r\n<\/pre>\n<p><\/code><\/p>\n<p>To run this:<\/p>\n<p><code><\/p>\n<pre>\r\ndef runDoctests():  \r\n    import sys\r\n    import doctest\r\n     failed, _ = doctest.testmod(optionflags=doctest.ELLIPSIS, verbose=None)\r\n    sys.exit(failed)\r\n\r\n\r\nif __name__ == '__main__': \r\n    runDoctests()\r\n<\/pre>\n<p><\/code><\/p>\n<p>It should exit normally if all your tests have been passed. If any of the test fails (see the example below), it will give you a report on where it fails.<\/p>\n<p>Example code with unexpected output (should have been &#8216;Cannot be converted to float&#8217; instead of &#8216;Not a point in 3D&#8217; ):<\/p>\n<p><code><\/p>\n<pre>\r\ndef Point3D(*args):\r\n    \"\"\"\r\n    &gt;&gt;&gt; Point3D(0,'A',0)\r\n    Not a point in 3D\r\n    \"\"\"\r\n    if len(args) != 3:\r\n        print('Not a point in 3D')\r\n        return;\r\n    else:\r\n        try:\r\n            return np.array(args,dtype=np.float64)\r\n        except:\r\n            print('Cannot be converted to float')\r\n<\/pre>\n<p><\/code><\/p>\n<p>The corresponding report from this code:<\/p>\n<p><code><\/p>\n<pre>\r\n**********************************************************************\r\nFile \"\/location\/test_doctest.py\", line 5, in __main__.Point3D\r\nFailed example:\r\n    Point3D(0,'A',0)\r\nExpected:\r\n    Not a point in 3D\r\nGot:\r\n    Cannot be converted to float\r\n**********************************************************************\r\n1 items had failures:\r\n   1 of   1 in __main__.Point3D\r\n***Test Failed*** 1 failures.\r\nAn exception has occurred, use %tb to see the full traceback.\r\n<\/pre>\n<p><\/code><\/p>\n<p>Here you go &#8211; a simple doctest demonstration that checks for the right kind of inputs and prints informative error messages<\/p>\n","protected":false},"excerpt":{"rendered":"<p>One of the ways to make your code more robust to unexpected input is to develop with boundary cases in your mind. Test-driven code development begins with writing a set of unit tests for each class. These tests often includes normal and extreme use cases. Thanks to packages like doctest for Python, Mocha and Jasmine [&hellip;]<\/p>\n","protected":false},"author":49,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"nf_dc_page":"","wikipediapreview_detectlinks":true,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"ngg_post_thumbnail":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[29,10,15],"tags":[],"ppma_author":[533],"class_list":["post-4459","post","type-post","status-publish","format-standard","hentry","category-code","category-groupmeetings","category-technical"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"authors":[{"term_id":533,"user_id":49,"is_guest":0,"slug":"catherine","display_name":"Wing Ki (Catherine) Wong","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/c620f4eb25c8d5c8efdae64e95c4ec436da2bc2d6654ab7085ad1a822e3b4341?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""}],"_links":{"self":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts\/4459","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/users\/49"}],"replies":[{"embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/comments?post=4459"}],"version-history":[{"count":5,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts\/4459\/revisions"}],"predecessor-version":[{"id":4848,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts\/4459\/revisions\/4848"}],"wp:attachment":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/media?parent=4459"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/categories?post=4459"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/tags?post=4459"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=4459"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}