Add introduction to testing and examples
authorJon Speicher <jon.speicher@gmail.com>
Fri, 26 Jul 2013 19:36:27 +0000 (15:36 -0400)
committerW. Trevor King <wking@tremily.us>
Sat, 9 Nov 2013 18:27:50 +0000 (10:27 -0800)
python/sw_engineering/SoftwareEngineering.ipynb

index 90ec73943691911812454ed467795ee10e1ba54a..b2c22e549eda04489962e29fe4784f7c596c6d3a 100644 (file)
        "output_type": "stream",
        "stream": "stdout",
        "text": [
-        "Writing sightings.py\n"
+        "Overwriting sightings.py\n"
        ]
       }
      ],
        ]
       }
      ],
-     "prompt_number": 12
+     "prompt_number": 4
     },
     {
      "cell_type": "markdown",
        ]
       }
      ],
-     "prompt_number": 13
+     "prompt_number": 5
     },
     {
      "cell_type": "code",
      "outputs": [
       {
        "output_type": "pyout",
-       "prompt_number": 14,
+       "prompt_number": 6,
        "text": [
         "(['2011-04-22', '2011-04-23', '2011-04-23', '2011-04-23', '2011-04-23'],\n",
         " ['21:06', '14:12', '10:24', '20:08', '18:46'],\n",
        ]
       }
      ],
-     "prompt_number": 14
+     "prompt_number": 6
     },
     {
      "cell_type": "markdown",
        ]
       }
      ],
-     "prompt_number": 15
+     "prompt_number": 7
     },
     {
      "cell_type": "markdown",
      "outputs": [
       {
        "output_type": "pyout",
-       "prompt_number": 16,
+       "prompt_number": 8,
        "text": [
         "117"
        ]
       }
      ],
-     "prompt_number": 16
+     "prompt_number": 8
     },
     {
      "cell_type": "markdown",
       "***"
      ]
     },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "***\n",
+      "# Testing\n",
+      "***\n",
+      "\n",
+      "We can tell that our new `read_sightings_from_file` function works because we imported it into IPython and ran it on a file with known contents, then examined the result. This is fine for one function, but what if we need to make changes to that function? What if we change a function that is called from many functions? Should we test all the calling functions too?\n",
+      "\n",
+      "If our goal is program correctness, the answer to all of these questions is \"yes\", but this will quickly become tedious. If we have dozens of functions, some of which call others, manual testing can be a nightmare, especially when considering boundary or error cases. A common, modern solution to this problem is to write a program to test our program. These programs are often referred to as *unit tests*.\n",
+      "\n",
+      "The benefits of unit testing are numerous:\n",
+      "\n",
+      "* Program correctness can be verified quickly\n",
+      "* Test coverage can be more complete\n",
+      "* Subtle bugs and interdependencies may be exposed by changes to seemingly-unrelated functions\n",
+      "* A test suite enables \"fearless refactoring\"\n",
+      "\n",
+      "What would this look like?"
+     ]
+    },
     {
      "cell_type": "code",
      "collapsed": false,
      "input": [
-      "def test_read_animals():\n",
-      "    date, time, animal, count = read_file('animals.txt')\n",
-      "    ref_date = ['2011-04-22', '2011-04-23', '2011-04-23', '2011-04-23', '2011-04-23']\n",
-      "    ref_time = ['21:06', '14:12', '10:24', '20:08', '18:46']\n",
-      "    ref_animal = ['Grizzly', 'Elk', 'Elk', 'Wolverine', 'Muskox']\n",
-      "    ref_count = [36, 25, 26, 31, 20]\n",
+      "def test_read_sightings_from_file():\n",
+      "    dates, times, animals, counts = sightings.read_sightings_from_file('animals.txt')\n",
+      "    if dates[0] == '2011-04-22':\n",
+      "        print 'Looks good!'\n",
+      "    else:\n",
+      "        print 'Unexpected date!'\n",
       "    \n",
-      "    assert date == ref_date, 'Dates do not match!'\n",
-      "    assert time == ref_time, 'Times do not match!'\n",
-      "    assert animal == ref_animal, 'Animals do not match!'\n",
-      "    assert count == ref_count, 'Counts do not match!'"
+      "test_read_sightings_from_file()"
      ],
      "language": "python",
      "metadata": {},
-     "outputs": [],
-     "prompt_number": 24
+     "outputs": [
+      {
+       "output_type": "stream",
+       "stream": "stdout",
+       "text": [
+        "Looks good!\n"
+       ]
+      }
+     ],
+     "prompt_number": 9
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "This is the core concept of unit testing: you create code that calls your code and verifies the results. We'll look at a number of conveniences that Python offers to streamline the process as we develop towards our goal of finding the mean number of sightings per animal. We'll start with a pre-canned example and modify it from there. Execute the cell below to create a file called `test_sightings.py`"
+     ]
     },
     {
      "cell_type": "code",
      "collapsed": false,
      "input": [
-      "test_read_animals()"
+      "%%file test_sightings.py\n",
+      "import sightings\n",
+      "\n",
+      "def test_read_sightings_from_file():\n",
+      "    expected_dates = ['2011-04-22', '2011-04-23', '2011-04-23', '2011-04-23', '2011-04-23']\n",
+      "    expected_times = ['21:06', '14:12', '10:24', '20:08', '18:46']\n",
+      "    expected_animals = ['Grizzly', 'Elk', 'Elk', 'Wolverine', 'Muskox']\n",
+      "    expected_counts = [36, 25, 26, 31, 20]\n",
+      "    \n",
+      "    dates, times, animals, counts = sightings.read_sightings_from_file('animals.txt')\n",
+      "    \n",
+      "    assert dates == expected_dates, 'Dates do not match!'\n",
+      "    assert times == expected_times, 'Times do not match!'\n",
+      "    assert animals == expected_animals, 'Animals do not match!'\n",
+      "    assert counts == expected_counts, 'Counts do not match!'"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [
+      {
+       "output_type": "stream",
+       "stream": "stdout",
+       "text": [
+        "Overwriting test_sightings.py\n"
+       ]
+      }
+     ],
+     "prompt_number": 10
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "**Note**: The act of testing to see if an actual result matches an expected result is so frequently used in unit testing that Python gives us the `assert` keyword to standardize and simplify the process."
+     ]
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "What does it look like if a test passes?"
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "import test_sightings\n",
+      "test_sightings.test_read_sightings_from_file()"
      ],
      "language": "python",
      "metadata": {},
      "outputs": [],
-     "prompt_number": 19
+     "prompt_number": 17
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "What does it look like if a test fails? "
+     ]
     },
     {
      "cell_type": "code",
      "collapsed": false,
      "input": [
-      "%load_ext ipython_nose"
+      "def add_two_plus_two():\n",
+      "    return 2 + 3\n",
+      "\n",
+      "def test_add_two_plus_two_equals_four():\n",
+      "    assert 4 == add_two_plus_two(), \"2 + 2 didn't equal 4\"\n",
+      "    \n",
+      "test_add_two_plus_two_equals_four()"
      ],
      "language": "python",
      "metadata": {},
      "outputs": [
       {
-       "output_type": "stream",
-       "stream": "stderr",
-       "text": [
-        "/Users/mrdavis/.homebrew/Cellar/python/2.7.3/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/nose/plugins/manager.py:418: UserWarning: Module argparse was already imported from /Users/mrdavis/.homebrew/Cellar/python/2.7.3/Frameworks/Python.framework/Versions/2.7/lib/python2.7/argparse.pyc, but /usr/local/lib/python2.7/site-packages is being added to sys.path\n",
-        "  import pkg_resources\n"
+       "ename": "AssertionError",
+       "evalue": "2 + 2 didn't equal 4",
+       "output_type": "pyerr",
+       "traceback": [
+        "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAssertionError\u001b[0m                            Traceback (most recent call last)",
+        "\u001b[0;32m<ipython-input-18-2d797333ff4c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      5\u001b[0m     \u001b[0;32massert\u001b[0m \u001b[0;36m4\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0madd_two_plus_two\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"2 + 2 didn't equal 4\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0mtest_add_two_plus_two_equals_four\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+        "\u001b[0;32m<ipython-input-18-2d797333ff4c>\u001b[0m in \u001b[0;36mtest_add_two_plus_two_equals_four\u001b[0;34m()\u001b[0m\n\u001b[1;32m      3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      4\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mtest_add_two_plus_two_equals_four\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m     \u001b[0;32massert\u001b[0m \u001b[0;36m4\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0madd_two_plus_two\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"2 + 2 didn't equal 4\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      7\u001b[0m \u001b[0mtest_add_two_plus_two_equals_four\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+        "\u001b[0;31mAssertionError\u001b[0m: 2 + 2 didn't equal 4"
        ]
       }
      ],
-     "prompt_number": 20
+     "prompt_number": 18
+    },
+    {
+     "cell_type": "markdown",
+     "metadata": {},
+     "source": [
+      "As you can see, the failed line is highlighted and the text supplied to the `assert` statement is printed. In addition, IPython provides what is known as a *traceback*. The traceback shows you which function called the function that called the function that called assert, ad infinitum. This can be a very helpful debugging aid."
+     ]
+    },
+    {
+     "cell_type": "code",
+     "collapsed": false,
+     "input": [
+      "%load_ext ipython_nose"
+     ],
+     "language": "python",
+     "metadata": {},
+     "outputs": [],
+     "prompt_number": 12
     },
     {
      "cell_type": "code",
      "outputs": [
       {
        "html": [
-        "<div id=\"ipython_nose_6f9af703139c456f96a60b847ccf532d\"></div>"
+        "<div id=\"ipython_nose_0807c237e17a408b8f8b351c1716ab5b\"></div>"
        ],
        "output_type": "display_data"
       },
       {
        "javascript": [
-        "document.ipython_nose_6f9af703139c456f96a60b847ccf532d = $(\"#ipython_nose_6f9af703139c456f96a60b847ccf532d\");"
+        "document.ipython_nose_0807c237e17a408b8f8b351c1716ab5b = $(\"#ipython_nose_0807c237e17a408b8f8b351c1716ab5b\");"
        ],
        "output_type": "display_data"
       },
       {
        "javascript": [
-        "document.ipython_nose_6f9af703139c456f96a60b847ccf532d.append($(\"<span>.</span>\"));"
+        "document.ipython_nose_0807c237e17a408b8f8b351c1716ab5b.append($(\"<span>.</span>\"));"
        ],
        "output_type": "display_data"
       },
       {
        "javascript": [
-        "delete document.ipython_nose_6f9af703139c456f96a60b847ccf532d;"
+        "delete document.ipython_nose_0807c237e17a408b8f8b351c1716ab5b;"
        ],
        "output_type": "display_data"
       },
         "    "
        ],
        "output_type": "pyout",
-       "prompt_number": 25,
+       "prompt_number": 13,
        "text": [
         "1/1 tests passed\n"
        ]
       }
      ],
-     "prompt_number": 25
+     "prompt_number": 13
     },
     {
      "cell_type": "code",