diff options
author | Prabhu Ramachandran | 2019-04-18 07:28:57 +0530 |
---|---|---|
committer | Prabhu Ramachandran | 2019-04-18 07:28:57 +0530 |
commit | 409bcfcb410f37e22e41b1d029420148ace304e6 (patch) | |
tree | 18d07f2f421cc437b1800330bcee33cfafee9633 | |
parent | 035c22dcfdddddcbffe02dc3e20934a2cf80a62d (diff) | |
download | sees-409bcfcb410f37e22e41b1d029420148ace304e6.tar.gz sees-409bcfcb410f37e22e41b1d029420148ace304e6.tar.bz2 sees-409bcfcb410f37e22e41b1d029420148ace304e6.zip |
Add advanced tdd material that was not checked in.
-rw-r--r-- | slides/test_driven_development/tdd_advanced.ipyml | 674 | ||||
-rw-r--r-- | slides/test_driven_development/tdd_advanced.ipynb | 844 |
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 +} |