{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "> This is one of the 100 recipes of the [IPython Cookbook](http://ipython-books.github.io/), the definitive guide to high-performance scientific computing and data science in Python.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 2.7. Writing unit tests with nose\n", "\n", "**This is the Python 3 version of the recipe.**\n", "\n", "Although Python has a native unit testing module (unittest), we will rather use Nose which is more convenient and more powerful. Having an extra dependency is not a problem as the Nose package is only required to launch the test suite, and not to use the software itself." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creation of the Python module" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%%writefile datautils.py\n", "# Version 1.\n", "import os\n", "from urllib.request import urlopen # Python 2: use urllib2\n", "\n", "def download(url):\n", " \"\"\"Download a file and save it in the current folder.\n", " Return the name of the downloaded file.\"\"\"\n", " # Get the filename.\n", " file = os.path.basename(url)\n", " # Download the file unless it already exists.\n", " if not os.path.exists(file):\n", " with open(file, 'w') as f:\n", " f.write(urlopen(url).read())\n", " return file" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creation of the test module" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%%writefile test_datautils.py\n", "from urllib.request import (HTTPHandler, install_opener, \n", " build_opener, addinfourl)\n", "import os\n", "import shutil\n", "import tempfile\n", "from io import StringIO # Python 2: use StringIO\n", "from datautils import download\n", "\n", "TEST_FOLDER = tempfile.mkdtemp()\n", "ORIGINAL_FOLDER = os.getcwd()\n", "\n", "class TestHTTPHandler(HTTPHandler):\n", " \"\"\"Mock HTTP handler.\"\"\"\n", " def http_open(self, req):\n", " resp = addinfourl(StringIO('test'), '', req.get_full_url(), 200)\n", " resp.msg = 'OK'\n", " return resp\n", " \n", "def setup():\n", " \"\"\"Install the mock HTTP handler for unit tests.\"\"\"\n", " install_opener(build_opener(TestHTTPHandler))\n", " os.chdir(TEST_FOLDER)\n", " \n", "def teardown():\n", " \"\"\"Restore the normal HTTP handler.\"\"\"\n", " install_opener(build_opener(HTTPHandler))\n", " # Go back to the original folder.\n", " os.chdir(ORIGINAL_FOLDER)\n", " # Delete the test folder.\n", " shutil.rmtree(TEST_FOLDER)\n", "\n", "def test_download1():\n", " file = download(\"http://example.com/file.txt\")\n", " # Check that the file has been downloaded.\n", " assert os.path.exists(file)\n", " # Check that the file contains the contents of the remote file.\n", " with open(file, 'r') as f:\n", " contents = f.read()\n", " print(contents)\n", " assert contents == 'test'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Launching the tests" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "!nosetests" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Adding a failing test" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, let's add a new test." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%%writefile test_datautils.py -a\n", "\n", "def test_download2():\n", " file = download(\"http://example.com/\")\n", " assert os.path.exists(file)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "!nosetests" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fixing the failing test" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The new test fails because the filename cannot be inferred from the URL, so we need to handle this case." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%%writefile datautils.py\n", "# Version 2.\n", "import os\n", "from urllib.request import urlopen # Python 2: use urllib2\n", "\n", "def download(url):\n", " \"\"\"Download a file and save it in the current folder.\n", " Return the name of the downloaded file.\"\"\"\n", " # Get the filename.\n", " file = os.path.basename(url)\n", " # Fix the bug, by specifying a fixed filename if the URL \n", " # does not contain one.\n", " if not file:\n", " file = 'downloaded'\n", " # Download the file unless it already exists.\n", " if not os.path.exists(file):\n", " with open(file, 'w') as f:\n", " f.write(urlopen(url).read())\n", " return file" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "!nosetests" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> You'll find all the explanations, figures, references, and much more in the book (to be released later this summer).\n", "\n", "> [IPython Cookbook](http://ipython-books.github.io/), by [Cyrille Rossant](http://cyrille.rossant.net), Packt Publishing, 2014 (500 pages)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.4.2" } }, "nbformat": 4, "nbformat_minor": 0 }