summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--slides/test_driven_development/tdd_advanced.ipyml674
-rw-r--r--slides/test_driven_development/tdd_advanced.ipynb844
2 files changed, 1518 insertions, 0 deletions
diff --git a/slides/test_driven_development/tdd_advanced.ipyml b/slides/test_driven_development/tdd_advanced.ipyml
new file mode 100644
index 0000000..a146951
--- /dev/null
+++ b/slides/test_driven_development/tdd_advanced.ipyml
@@ -0,0 +1,674 @@
+cells:
+
+- markdown: |
+ # Advanced topics in test driven development
+
+ metadata:
+ slideshow:
+ slide_type: slide
+
+- markdown: |
+ ## Introduction
+
+
+ - Already seen the basics
+ - Learn some advanced topics
+
+ metadata:
+ slideshow:
+ slide_type: slide
+
+- markdown: |
+ ## The hypothesis package
+
+ - http://hypothesis.readthedocs.io
+
+ - `pip install hypothesis`
+
+ - General idea earlier:
+ - Make test data.
+ - Perform operations
+ - Assert something after operation
+
+ - Hypothesis automates this!
+ - Describe range of scenarios
+ - Computer explores these and tests
+
+ - With hypothesis:
+ - Generate random data using specification
+ - Perform operations
+ - assert something about result
+
+
+ metadata:
+ slideshow:
+ slide_type: slide
+
+- markdown: |
+ ### Example
+
+ metadata:
+ slideshow:
+ slide_type: slide
+
+- code: |
+
+ from hypothesis import given
+ from hypothesis import strategies as st
+
+ from gcd import gcd
+
+ @given(st.integers(min_value=0), st.integers(min_value=0))
+ def test_gcd(a, b):
+ result = gcd(a, b)
+ # Now what?
+ # assert a%result == 0
+
+ id: 0
+ metadata:
+ collapsed: true
+ slideshow:
+ slide_type: fragment
+
+- markdown: |
+ ### Example: adding a specific case
+
+ metadata:
+ slideshow:
+ slide_type: slide
+
+- code: |
+ @given(st.integers(min_value=0), st.integers(min_value=0))
+
+ @example(a=44, b=19)
+
+ def test_gcd(a, b):
+ result = gcd(a, b)
+ # Now what?
+ # assert a%result == 0
+
+ id: 1
+ metadata:
+ collapsed: true
+ slideshow:
+ slide_type: fragment
+
+- markdown: |
+ ### More details
+
+ - `given` generates inputs
+ - `strategies`: provides a strategy for inputs
+ - Different stratiegies
+ - `integers`
+ - `floats`
+ - `text`
+ - `booleans`
+ - `tuples`
+ - `lists`
+ - ...
+
+ - See: http://hypothesis.readthedocs.io/en/latest/data.html
+
+ metadata:
+ slideshow:
+ slide_type: slide
+
+- markdown: |
+ ### Example exercise
+
+ - Write a simple run-length encoding function called `encode`
+ - Write another called `decode` to produce the same input from the output of
+ `encode`
+
+ metadata:
+ slideshow:
+ slide_type: slide
+
+- code: |
+ def encode(text):
+ return []
+
+ def decode(lst):
+ return ''
+
+
+ id: 2
+ metadata:
+ collapsed: true
+ slideshow:
+ slide_type: fragment
+
+- markdown: |
+ ### The test
+
+
+- code: |
+ from hypothesis import given
+ from hypothesis import strategies as st
+
+ @given(st.text())
+ def test_decode_inverts_encode(s):
+ assert decode(encode(s)) == s
+
+ id: 3
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### Summary
+
+ - Much easier to test
+ - hypothesis does the hard work
+ - Can do a lot more!
+ - Read the docs for more
+ - For some detailed articles:
+ http://hypothesis.works/articles/intro/
+ - Here in particular is one interesting article:
+ http://hypothesis.works/articles/calculating-the-mean/
+
+ ----
+
+ ## Unittest module
+
+ - Basic idea and style is from JUnit
+ - Some consider this old style
+
+
+ ### How to use unittest
+
+ - Subclass `unittest.TestCase`
+ - Create test methods
+
+ ### A simple example
+
+ - Let us test gcd.py with unittest
+
+
+- code: |
+ # gcd.py
+ def gcd(a, b):
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+
+ id: 4
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### Writing the test
+
+
+- code: |
+ # test_gcd.py
+ from gcd import gcd
+ import unittest
+
+ class TestGCD(unittest.TestCase):
+ def test_gcd_works_for_positive_integers(self):
+ self.assertEqual(gcd(48, 64), 16)
+ self.assertEqual(gcd(44, 19), 1)
+
+ if __name__ == '__main__':
+ unittest.main()
+
+ id: 5
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### Running it
+
+ - Just run `python test_gcd.py`
+ - Also works with `nosetests` and `pytest`
+
+
+ ### Notes
+
+ - Note the name of the method.
+ - Note the use of `self.assertEqual`
+ - Also available: `assertNotEqual, assertTrue, assertFalse, assertIs, assertIsNot`
+ - `assertIsNone, assertIn, assertIsInstance, assertRaises`
+ - `assertAlmostEqual, assertListEqual, assertSequenceEqual ` ...
+
+ - https://docs.python.org/2/library/unittest.html
+
+
+ ### Fixtures
+
+ - What if you want to do something common before all tests?
+ - Typically called a **fixture**
+
+ - Use the `setUp` and `tearDown` methods for method-level fixtures
+
+ ### Silly fixture example
+
+
+- code: |
+ # test_gcd.py
+ import gcd
+ import unittest
+
+ class TestGCD(unittest.TestCase):
+ def setUp(self):
+ print("setUp")
+ def tearDown(self):
+ print("tearDown")
+ def test_gcd_works_for_positive_integers(self):
+ self.assertEqual(gcd(48, 64), 16)
+ self.assertEqual(gcd(44, 19), 1)
+
+ if __name__ == '__main__':
+ unittest.main()
+
+ id: 6
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### Exercise
+
+ - Fix bug with negative numbers in gcd.py.
+ - Use TDD.
+
+
+ ### Using hypothesis with unittest
+
+
+- code: |
+ # test_gcd.py
+ from hypothesis import given
+ from hypothesis import strategies as st
+
+ import gcd
+ import unittest
+
+ class TestGCD(unittest.TestCase):
+ @given(a=st.integers(min_value=0), b=st.integers(min_value=0))
+ def test_gcd_works_for_positive_integers(self, a, b):
+ result = gcd(a, b)
+ assert a%result == 0
+ assert b%result == 0
+ assert result <= a and result <= b
+
+ if __name__ == '__main__':
+ unittest.main()
+
+ id: 7
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### Some notes on style
+
+ - Use descriptive function names
+ - Intent matters
+
+ - Segregate the test code into the following
+
+
+- code: |
+ - Given: what is the context of the test?
+ - When: what action is taken to actually test the problem
+ - Then: what do we actually ensure.
+
+ id: 8
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### More on intent driven programming
+
+ - "Programs must be written for people to read, and only incidentally for
+ machines to execute.” Harold Abelson
+
+ - The code should make the intent clear.
+
+ For example:
+
+
+- code: |
+ if self.temperature > 600 and self.pressure > 10e5:
+ message = 'hello you have a problem here!'
+ message += 'current temp is %s'%(self.temperature)
+ print(message)
+ self.valve.close()
+ self.raise_warning()
+
+ id: 9
+ metadata:
+ collapsed: true
+
+- markdown: |
+ is totally unclear as to the intent. Instead refactor as follows:
+
+
+- code: |
+ if self.reactor_is_critical():
+ self.shutdown_with_warning()
+
+ id: 10
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### A more involved testing example
+
+ - Motivational problem:
+
+ > Find all the git repositories inside a given directory recursively.
+ > Make this a command line tool supporting command line use.
+
+ - Write tests for the code
+ - Some rules:
+
+ 0. The test should be runnable by anyone (even by a computer), almost anywhere.
+ 1. Don't write anything in the current directory (use a temporary directory).
+ 2. Cleanup any files you create while testing.
+ 3. Make sure tests do not affect global state too much.
+
+
+ ### Solution
+
+ 1. Create some test data.
+ 2. Test!
+ 3. Cleanup the test data
+
+
+ ### Class-level fixtures
+
+ - Use `setupClass` and `tearDownClass` classmethods for class level fixtures.
+
+
+ ### Module-level fixtures
+
+ - `setup_module`, `teardown_module`
+ - Can be used for a module-level fixture
+
+ - http://nose.readthedocs.io/en/latest/writing_tests.html
+
+
+ ## Coverage
+
+ - Assess the amount of code that is covered by testing
+ - http://coverage.readthedocs.io/
+ - `pip install coverage`
+ - Integrates with nosetests/pytest
+
+ ### Typical coverage usage
+
+
+- code: |
+ $ coverage run -m nose.core my_package
+ $ coverage report -m
+ $ coverage html
+
+ id: 11
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ## mock
+
+ - Mocking for advanced testing.
+
+ - Example: reading some twitter data
+ - Example: function to post an update to facebook or twitter
+ - Example: email user when simulation crashes
+
+ - Can you test it? How?
+
+ ### Using mock: the big picture
+
+ - Do you really want to post something on facebook?
+ - Or do you want to know if the right method was called with the right arguments?
+
+ - Idea: "mock" the objects that do something and test them
+
+ - Quoting from the Python docs:
+
+ > It allows you to replace parts of your system under test with mock objects
+ > and make assertions about how they have been used.
+
+ ### Installation
+
+ - Built-in on Python >= 3.3
+
+
+- code: |
+ - `from unittest import mock`
+
+ id: 12
+ metadata:
+ collapsed: true
+
+- markdown: |
+ - else `pip install mock`
+
+
+- code: |
+ - `import mock`
+
+ id: 13
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### Simple examples
+
+ Say we have a class:
+
+
+- code: |
+ class ProductionClass(object):
+ def method(self, *args):
+ # This does something we do not want to actually run in the test
+ # ...
+ pass
+
+ id: 14
+ metadata:
+ collapsed: true
+
+- markdown: |
+ To mock the `ProductionClass.method` do this:
+
+
+- code: |
+ from unittest.mock import MagicMock
+ thing = ProductionClass()
+ thing.method = MagicMock(return_value=3)
+ thing.method(3, 4, 5, key='value')
+ thing.method.assert_called_with(3, 4, 5, key='value')
+
+ id: 15
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### More practical use case
+
+ - Mocking a module or system call
+ - Mocking an object or method
+ - Remember that after testing you want to restore original state
+ - Use `mock.patch`
+
+ ### An example
+
+ - Write code to remove generated files from LaTeX compilation, i.e. remove the
+ *.aux, *.log, *.pdf etc.
+
+ Here is a simple attempt:
+
+
+- code: |
+ # clean_tex.py
+ import os
+
+ def cleanup(tex_file_pth):
+ base = os.path.splitext(tex_file_pth)[0]
+ for ext in ('.aux', '.log'):
+ f = base + ext
+ if os.path.exists(f):
+ os.remove(f)
+
+ id: 16
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### Testing this with mock
+
+
+- code: |
+ import mock
+
+ @mock.patch('clean_tex.os.remove')
+ def test_cleanup_removes_extra_files(mock_remove):
+ cleanup('foo.tex')
+
+ expected = [mock.call('foo.' + x) for x in ('aux', 'log')]
+
+ mock_remove.assert_has_calls(expected)
+
+ id: 17
+ metadata:
+ collapsed: true
+
+- markdown: |
+ - Note the mocked argument that is passed.
+ - Note that we did not mock `os.remove`
+ - Mock where the object is looked up
+
+ ### Doing more
+
+
+- code: |
+ import mock
+
+ @mock.patch('clean_tex.os.path')
+ @mock.patch('clean_tex.os.remove')
+ def test_cleanup_does_not_fail_when_files_dont_exist(mock_remove, mock_path):
+ # Setup the mock_path to return False
+ mock_path.exists.return_value = False
+
+ cleanup('foo.tex')
+
+ mock_remove.assert_not_called()
+
+ id: 18
+ metadata:
+ collapsed: true
+
+- markdown: |
+ - Note the order of the passed arguments
+ - Note the name of the method
+
+ ### Patching instance methods
+
+ Use `mock.patch.object` to patch an instance method
+
+
+- code: |
+ @mock.patch.object(ProductionClass, 'method')
+ def test_method(mock_method):
+ obj = ProductionClass()
+ obj.method(1)
+
+ mock_method.assert_called_once_with(1)
+
+ id: 19
+ metadata:
+ collapsed: true
+
+- markdown: |
+ Mock works as a context manager:
+
+
+- code: |
+ with mock.patch.object(ProductionClass, 'method') as mock_method:
+ obj = ProductionClass()
+ obj.method(1)
+
+ mock_method.assert_called_once_with(1)
+
+ id: 20
+ metadata:
+ collapsed: true
+
+- markdown: |
+ ### More articles on mock
+
+ - See more here https://docs.python.org/3/library/unittest.mock.html
+ - https://www.toptal.com/python/an-introduction-to-mocking-in-python
+
+
+ ## Pytest
+
+ Offers many useful and convenient features that are useful
+
+
+ ## Odds and ends
+
+ ### Linters
+
+ - `pyflakes`
+ - `flake8`
+
+ ### IPython goodies
+
+ - Use `%run`
+ - Use `%pdb`
+ - `%debug`
+
+ ### Debugging
+
+ - Debug with `%run`
+ - pdb.set_trace()
+ - IPython set trace:
+
+
+- code: |
+ from IPython.core.debugger import Tracer; Tracer()()
+
+ id: 21
+ metadata:
+ collapsed: true
+
+- markdown: |
+ - See here: http://www.scipy-lectures.org/advanced/debugging/
+
+
+# The lines below here may be deleted if you do not need them.
+# ---------------------------------------------------------------------------
+metadata:
+ celltoolbar: Slideshow
+ kernelspec:
+ display_name: Python 2
+ language: python
+ name: python2
+ language_info:
+ codemirror_mode:
+ name: ipython
+ version: 2
+ file_extension: .py
+ mimetype: text/x-python
+ name: python
+ nbconvert_exporter: python
+ pygments_lexer: ipython2
+ version: 2.7.11
+nbformat: 4
+nbformat_minor: 0
+
+# ---------------------------------------------------------------------------
+data:
+ [{execution_count: null, outputs: []}, {execution_count: null, outputs: []}, {execution_count: null,
+ outputs: []}, {execution_count: null, outputs: []}, {execution_count: null, outputs: []},
+ {execution_count: null, outputs: []}, {execution_count: null, outputs: []}, {execution_count: null,
+ outputs: []}, {execution_count: null, outputs: []}, {execution_count: null, outputs: []},
+ {execution_count: null, outputs: []}, {execution_count: null, outputs: []}, {execution_count: null,
+ outputs: []}, {execution_count: null, outputs: []}, {execution_count: null, outputs: []},
+ {execution_count: null, outputs: []}, {execution_count: null, outputs: []}, {execution_count: null,
+ outputs: []}, {execution_count: null, outputs: []}, {execution_count: null, outputs: []},
+ {execution_count: null, outputs: []}, {execution_count: null, outputs: []}]
+
diff --git a/slides/test_driven_development/tdd_advanced.ipynb b/slides/test_driven_development/tdd_advanced.ipynb
new file mode 100644
index 0000000..e780dc8
--- /dev/null
+++ b/slides/test_driven_development/tdd_advanced.ipynb
@@ -0,0 +1,844 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "# Advanced topics in test driven development"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "## Introduction\n",
+ "\n",
+ "\n",
+ "- Already seen the basics\n",
+ "- Learn some advanced topics"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "## The hypothesis package\n",
+ "\n",
+ "- http://hypothesis.readthedocs.io\n",
+ "\n",
+ "- `pip install hypothesis`\n",
+ "\n",
+ "- General idea earlier:\n",
+ " - Make test data.\n",
+ " - Perform operations\n",
+ " - Assert something after operation\n",
+ "\n",
+ "- Hypothesis automates this!\n",
+ " - Describe range of scenarios\n",
+ " - Computer explores these and tests\n",
+ "\n",
+ "- With hypothesis:\n",
+ " - Generate random data using specification\n",
+ " - Perform operations\n",
+ " - assert something about result\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "### Example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true,
+ "slideshow": {
+ "slide_type": "fragment"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "\n",
+ "from hypothesis import given\n",
+ "from hypothesis import strategies as st\n",
+ "\n",
+ "from gcd import gcd\n",
+ "\n",
+ "@given(st.integers(min_value=0), st.integers(min_value=0))\n",
+ "def test_gcd(a, b):\n",
+ " result = gcd(a, b)\n",
+ " # Now what?\n",
+ " # assert a%result == 0\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "### Example: adding a specific case"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true,
+ "slideshow": {
+ "slide_type": "fragment"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "@given(st.integers(min_value=0), st.integers(min_value=0))\n",
+ "\n",
+ "@example(a=44, b=19)\n",
+ "\n",
+ "def test_gcd(a, b):\n",
+ " result = gcd(a, b)\n",
+ " # Now what?\n",
+ " # assert a%result == 0\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "### More details\n",
+ "\n",
+ "- `given` generates inputs\n",
+ "- `strategies`: provides a strategy for inputs\n",
+ "- Different stratiegies\n",
+ " - `integers`\n",
+ " - `floats`\n",
+ " - `text`\n",
+ " - `booleans`\n",
+ " - `tuples`\n",
+ " - `lists`\n",
+ " - ...\n",
+ "\n",
+ "- See: http://hypothesis.readthedocs.io/en/latest/data.html"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "slideshow": {
+ "slide_type": "slide"
+ }
+ },
+ "source": [
+ "### Example exercise\n",
+ "\n",
+ "- Write a simple run-length encoding function called `encode`\n",
+ "- Write another called `decode` to produce the same input from the output of\n",
+ " `encode`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true,
+ "slideshow": {
+ "slide_type": "fragment"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "def encode(text):\n",
+ " return []\n",
+ "\n",
+ "def decode(lst):\n",
+ " return ''\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### The test"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "from hypothesis import given\n",
+ "from hypothesis import strategies as st\n",
+ "\n",
+ "@given(st.text())\n",
+ "def test_decode_inverts_encode(s):\n",
+ " assert decode(encode(s)) == s"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Summary\n",
+ "\n",
+ "- Much easier to test\n",
+ "- hypothesis does the hard work\n",
+ "- Can do a lot more!\n",
+ "- Read the docs for more\n",
+ "- For some detailed articles:\n",
+ " http://hypothesis.works/articles/intro/\n",
+ "- Here in particular is one interesting article:\n",
+ " http://hypothesis.works/articles/calculating-the-mean/\n",
+ "\n",
+ "----\n",
+ "\n",
+ "## Unittest module\n",
+ "\n",
+ "- Basic idea and style is from JUnit\n",
+ "- Some consider this old style\n",
+ "\n",
+ "\n",
+ "### How to use unittest\n",
+ "\n",
+ "- Subclass `unittest.TestCase`\n",
+ "- Create test methods\n",
+ "\n",
+ "### A simple example\n",
+ "\n",
+ "- Let us test gcd.py with unittest"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "# gcd.py\n",
+ "def gcd(a, b):\n",
+ " if b == 0:\n",
+ " return a\n",
+ " return gcd(b, a%b)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Writing the test"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "# test_gcd.py\n",
+ "from gcd import gcd\n",
+ "import unittest\n",
+ "\n",
+ "class TestGCD(unittest.TestCase):\n",
+ " def test_gcd_works_for_positive_integers(self):\n",
+ " self.assertEqual(gcd(48, 64), 16)\n",
+ " self.assertEqual(gcd(44, 19), 1)\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " unittest.main()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Running it\n",
+ "\n",
+ "- Just run `python test_gcd.py`\n",
+ "- Also works with `nosetests` and `pytest`\n",
+ "\n",
+ "\n",
+ "### Notes\n",
+ "\n",
+ "- Note the name of the method.\n",
+ "- Note the use of `self.assertEqual`\n",
+ "- Also available: `assertNotEqual, assertTrue, assertFalse, assertIs, assertIsNot`\n",
+ "- `assertIsNone, assertIn, assertIsInstance, assertRaises`\n",
+ "- `assertAlmostEqual, assertListEqual, assertSequenceEqual ` ...\n",
+ "\n",
+ "- https://docs.python.org/2/library/unittest.html\n",
+ "\n",
+ "\n",
+ "### Fixtures\n",
+ "\n",
+ "- What if you want to do something common before all tests?\n",
+ "- Typically called a **fixture**\n",
+ "\n",
+ "- Use the `setUp` and `tearDown` methods for method-level fixtures\n",
+ "\n",
+ "### Silly fixture example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "# test_gcd.py\n",
+ "import gcd\n",
+ "import unittest\n",
+ "\n",
+ "class TestGCD(unittest.TestCase):\n",
+ " def setUp(self):\n",
+ " print(\"setUp\")\n",
+ " def tearDown(self):\n",
+ " print(\"tearDown\")\n",
+ " def test_gcd_works_for_positive_integers(self):\n",
+ " self.assertEqual(gcd(48, 64), 16)\n",
+ " self.assertEqual(gcd(44, 19), 1)\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " unittest.main()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Exercise\n",
+ "\n",
+ "- Fix bug with negative numbers in gcd.py.\n",
+ "- Use TDD.\n",
+ "\n",
+ "\n",
+ "### Using hypothesis with unittest"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "# test_gcd.py\n",
+ "from hypothesis import given\n",
+ "from hypothesis import strategies as st\n",
+ "\n",
+ "import gcd\n",
+ "import unittest\n",
+ "\n",
+ "class TestGCD(unittest.TestCase):\n",
+ " @given(a=st.integers(min_value=0), b=st.integers(min_value=0))\n",
+ " def test_gcd_works_for_positive_integers(self, a, b):\n",
+ " result = gcd(a, b)\n",
+ " assert a%result == 0\n",
+ " assert b%result == 0\n",
+ " assert result <= a and result <= b\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " unittest.main()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Some notes on style\n",
+ "\n",
+ "- Use descriptive function names\n",
+ "- Intent matters\n",
+ "\n",
+ "- Segregate the test code into the following"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "- Given: what is the context of the test?\n",
+ "- When: what action is taken to actually test the problem\n",
+ "- Then: what do we actually ensure."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### More on intent driven programming\n",
+ "\n",
+ "- \"Programs must be written for people to read, and only incidentally for\n",
+ " machines to execute.” Harold Abelson\n",
+ "\n",
+ "- The code should make the intent clear.\n",
+ "\n",
+ "For example:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "if self.temperature > 600 and self.pressure > 10e5:\n",
+ " message = 'hello you have a problem here!'\n",
+ " message += 'current temp is %s'%(self.temperature)\n",
+ " print(message)\n",
+ " self.valve.close()\n",
+ " self.raise_warning()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "is totally unclear as to the intent. Instead refactor as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "if self.reactor_is_critical():\n",
+ " self.shutdown_with_warning()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### A more involved testing example\n",
+ "\n",
+ "- Motivational problem:\n",
+ "\n",
+ "> Find all the git repositories inside a given directory recursively.\n",
+ "> Make this a command line tool supporting command line use.\n",
+ "\n",
+ "- Write tests for the code\n",
+ "- Some rules:\n",
+ "\n",
+ " 0. The test should be runnable by anyone (even by a computer), almost anywhere.\n",
+ " 1. Don't write anything in the current directory (use a temporary directory).\n",
+ " 2. Cleanup any files you create while testing.\n",
+ " 3. Make sure tests do not affect global state too much.\n",
+ "\n",
+ "\n",
+ "### Solution\n",
+ "\n",
+ "1. Create some test data.\n",
+ "2. Test!\n",
+ "3. Cleanup the test data\n",
+ "\n",
+ "\n",
+ "### Class-level fixtures\n",
+ "\n",
+ "- Use `setupClass` and `tearDownClass` classmethods for class level fixtures.\n",
+ "\n",
+ "\n",
+ "### Module-level fixtures\n",
+ "\n",
+ "- `setup_module`, `teardown_module`\n",
+ "- Can be used for a module-level fixture\n",
+ "\n",
+ "- http://nose.readthedocs.io/en/latest/writing_tests.html\n",
+ "\n",
+ "\n",
+ "## Coverage\n",
+ "\n",
+ "- Assess the amount of code that is covered by testing\n",
+ "- http://coverage.readthedocs.io/\n",
+ "- `pip install coverage`\n",
+ "- Integrates with nosetests/pytest\n",
+ "\n",
+ "### Typical coverage usage"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "$ coverage run -m nose.core my_package\n",
+ "$ coverage report -m\n",
+ "$ coverage html"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## mock\n",
+ "\n",
+ "- Mocking for advanced testing.\n",
+ "\n",
+ "- Example: reading some twitter data\n",
+ "- Example: function to post an update to facebook or twitter\n",
+ "- Example: email user when simulation crashes\n",
+ "\n",
+ "- Can you test it? How?\n",
+ "\n",
+ "### Using mock: the big picture\n",
+ "\n",
+ "- Do you really want to post something on facebook?\n",
+ "- Or do you want to know if the right method was called with the right arguments?\n",
+ "\n",
+ "- Idea: \"mock\" the objects that do something and test them\n",
+ "\n",
+ "- Quoting from the Python docs:\n",
+ "\n",
+ "> It allows you to replace parts of your system under test with mock objects\n",
+ "> and make assertions about how they have been used.\n",
+ "\n",
+ "### Installation\n",
+ "\n",
+ "- Built-in on Python >= 3.3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "- `from unittest import mock`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "- else `pip install mock`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "- `import mock`\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Simple examples\n",
+ "\n",
+ "Say we have a class:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "class ProductionClass(object):\n",
+ " def method(self, *args):\n",
+ " # This does something we do not want to actually run in the test\n",
+ " # ...\n",
+ " pass"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To mock the `ProductionClass.method` do this:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "from unittest.mock import MagicMock\n",
+ "thing = ProductionClass()\n",
+ "thing.method = MagicMock(return_value=3)\n",
+ "thing.method(3, 4, 5, key='value')\n",
+ "thing.method.assert_called_with(3, 4, 5, key='value')\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### More practical use case\n",
+ "\n",
+ "- Mocking a module or system call\n",
+ "- Mocking an object or method\n",
+ "- Remember that after testing you want to restore original state\n",
+ "- Use `mock.patch`\n",
+ "\n",
+ "### An example\n",
+ "\n",
+ "- Write code to remove generated files from LaTeX compilation, i.e. remove the\n",
+ " *.aux, *.log, *.pdf etc.\n",
+ "\n",
+ "Here is a simple attempt:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "# clean_tex.py\n",
+ "import os\n",
+ "\n",
+ "def cleanup(tex_file_pth):\n",
+ " base = os.path.splitext(tex_file_pth)[0]\n",
+ " for ext in ('.aux', '.log'):\n",
+ " f = base + ext\n",
+ " if os.path.exists(f):\n",
+ " os.remove(f)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Testing this with mock"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "import mock\n",
+ "\n",
+ "@mock.patch('clean_tex.os.remove')\n",
+ "def test_cleanup_removes_extra_files(mock_remove):\n",
+ " cleanup('foo.tex')\n",
+ "\n",
+ " expected = [mock.call('foo.' + x) for x in ('aux', 'log')]\n",
+ "\n",
+ " mock_remove.assert_has_calls(expected)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "- Note the mocked argument that is passed.\n",
+ "- Note that we did not mock `os.remove`\n",
+ "- Mock where the object is looked up\n",
+ "\n",
+ "### Doing more"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "import mock\n",
+ "\n",
+ "@mock.patch('clean_tex.os.path')\n",
+ "@mock.patch('clean_tex.os.remove')\n",
+ "def test_cleanup_does_not_fail_when_files_dont_exist(mock_remove, mock_path):\n",
+ " # Setup the mock_path to return False\n",
+ " mock_path.exists.return_value = False\n",
+ "\n",
+ " cleanup('foo.tex')\n",
+ "\n",
+ " mock_remove.assert_not_called()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "- Note the order of the passed arguments\n",
+ "- Note the name of the method\n",
+ "\n",
+ "### Patching instance methods\n",
+ "\n",
+ "Use `mock.patch.object` to patch an instance method"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "@mock.patch.object(ProductionClass, 'method')\n",
+ "def test_method(mock_method):\n",
+ " obj = ProductionClass()\n",
+ " obj.method(1)\n",
+ "\n",
+ " mock_method.assert_called_once_with(1)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Mock works as a context manager:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "with mock.patch.object(ProductionClass, 'method') as mock_method:\n",
+ " obj = ProductionClass()\n",
+ " obj.method(1)\n",
+ "\n",
+ "mock_method.assert_called_once_with(1)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### More articles on mock\n",
+ "\n",
+ "- See more here https://docs.python.org/3/library/unittest.mock.html\n",
+ "- https://www.toptal.com/python/an-introduction-to-mocking-in-python\n",
+ "\n",
+ "\n",
+ "## Pytest\n",
+ "\n",
+ "Offers many useful and convenient features that are useful\n",
+ "\n",
+ "\n",
+ "## Odds and ends\n",
+ "\n",
+ "### Linters\n",
+ "\n",
+ "- `pyflakes`\n",
+ "- `flake8`\n",
+ "\n",
+ "### IPython goodies\n",
+ "\n",
+ "- Use `%run`\n",
+ "- Use `%pdb`\n",
+ "- `%debug`\n",
+ "\n",
+ "### Debugging\n",
+ "\n",
+ "- Debug with `%run`\n",
+ "- pdb.set_trace()\n",
+ "- IPython set trace:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "from IPython.core.debugger import Tracer; Tracer()()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "- See here: http://www.scipy-lectures.org/advanced/debugging/"
+ ]
+ }
+ ],
+ "metadata": {
+ "celltoolbar": "Slideshow",
+ "kernelspec": {
+ "display_name": "Python 2",
+ "language": "python",
+ "name": "python2"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 2
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython2",
+ "version": "2.7.11"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}