summaryrefslogtreecommitdiff
path: root/lecture-notes/tdd
diff options
context:
space:
mode:
authorHardik Ghaghada2014-06-12 13:22:13 +0530
committerHardik Ghaghada2014-06-12 13:22:13 +0530
commit1f7318ca9553270899537d98d75e9f4fced85ed4 (patch)
tree7d149f9203f7eb122e749696e570d10086275ce4 /lecture-notes/tdd
parent985adfa4f8a8b9cfba2b0a573dadc77283651957 (diff)
downloadsees-1f7318ca9553270899537d98d75e9f4fced85ed4.tar.gz
sees-1f7318ca9553270899537d98d75e9f4fced85ed4.tar.bz2
sees-1f7318ca9553270899537d98d75e9f4fced85ed4.zip
restructring repo
Diffstat (limited to 'lecture-notes/tdd')
-rw-r--r--lecture-notes/tdd/generate_testcases.py36
-rw-r--r--lecture-notes/tdd/lab-workbook.rst146
-rw-r--r--lecture-notes/tdd/math_utils/gcd.py22
-rw-r--r--lecture-notes/tdd/math_utils/gcd_testcases.dat50
-rw-r--r--lecture-notes/tdd/math_utils/test_gcd.py29
-rw-r--r--lecture-notes/tdd/tdd.rst656
-rw-r--r--lecture-notes/tdd/tdd.tex514
7 files changed, 1453 insertions, 0 deletions
diff --git a/lecture-notes/tdd/generate_testcases.py b/lecture-notes/tdd/generate_testcases.py
new file mode 100644
index 0000000..17b48a7
--- /dev/null
+++ b/lecture-notes/tdd/generate_testcases.py
@@ -0,0 +1,36 @@
+import random
+
+def gcd(a, b):
+ while b != 0:
+ a, b = b, a % b
+ return a
+
+a = random.sample(xrange(100), 10)
+b = random.sample(xrange(1000), 10)
+c = random.sample(xrange(10000), 10)
+c = random.sample(xrange(10000), 10)
+d = random.sample(xrange(100000), 10)
+e = random.sample(xrange(1000000), 10)
+f = a + b + c + d + e
+f.sort()
+a = random.sample(xrange(100), 10)
+b = random.sample(xrange(1000), 10)
+c = random.sample(xrange(10000), 10)
+d = random.sample(xrange(100000), 10)
+e = random.sample(xrange(1000000), 10)
+g = a + b + c + d + e
+
+testcases = []
+for item in f:
+ a = f[random.randrange(0, len(f))]
+ b = g[random.randrange(0, len(g))]
+ gc = gcd(a, b)
+ testcases.append([a, b, gc])
+
+sortedcases = sorted(testcases, key=lambda case: case[0])
+
+fil = open('/home/madhu/Desktop/gcdtest.dat', 'w')
+for case in sortedcases:
+ fil.write('%d, %d, %d\n' % (case[0], case[1], case[2]))
+
+fil.close()
diff --git a/lecture-notes/tdd/lab-workbook.rst b/lecture-notes/tdd/lab-workbook.rst
new file mode 100644
index 0000000..fa5afa4
--- /dev/null
+++ b/lecture-notes/tdd/lab-workbook.rst
@@ -0,0 +1,146 @@
+======================================
+Lab Workbook - Test Driven Development
+======================================
+
+The notation that follows every question denotes the level on the
+Revised Bloom's Taxonomy.
+
+Lab - 1
+=======
+
+ 1. Write a stub function for calculating the LCM of two numbers.
+ - U-level
+ 2. Write the tests for the LCM function, place the tests in if
+ __name__ == '__main__': part of the Python file. Demonstrate that
+ the tests fail. - U-level
+ 3. Implement the code for the LCM function, using the gcd function
+ provided in the examples in the chapter. Demonstrate the tests
+ pass. (For the algorithm refer to Wikipedia - [0]) - Ap-level
+ 4. Alternatively, build a set of test cases, preferably a large
+ number of cases, place it in a text file and use these test cases
+ to test your LCM function. Demonstrate that tests still continue
+ to pass. - U-level
+
+[0] - http://en.wikipedia.org/wiki/Least_common_multiple#Reduction_by_the_greatest_common_divisor
+
+Lab - 2
+=======
+
+ 1. Write the stub function, followed by the tests(demonstrating the
+ failed tests), in turn followed by the code(demonstrating the
+ passing tests) to calculate the number of days between two
+ dates. Name your function num_of_days(). The function should take
+ two arguments, both being tuples. Each tuple represents the date
+ in the format of (dd, mm, yyyy) where dd, mm and yyyy are
+ integers. - Ap-level
+
+ 2. Rewrite the num_of_days() function to take the start date as an
+ optional argument. If the start date is not specified calculate
+ the number of days between the only specified date since Unix
+ epoch. Prior to manipulating the code to do this, make sure you
+ change the tests, make them fail and then refactor the code.
+ - Ap-level
+
+
+Lab -3
+======
+
+ 1. Move the tests that were written to GCD function in the examples
+ of this chapter to a separate function called test_gcd(). Do the
+ same for LCM function and num_of_days() function. Make sure when
+ the respective Python files are executed as stand alone scripts
+ these tests executed. - U-level
+ 2. Put all these files in a single directory called utils and run
+ the nosetests command. Make a report of the results. - U-level
+ 3. Write doctests to each of the above functions. Demonstrate and
+ report the results as executed by running the doctests using
+ doctest.testmod() function and using nosetests command. -Ap-level
+
+Lab - 4
+=======
+
+ 1. Consider the following use case: We are given a large list of
+ items called *data* where each item is a again a list with three
+ values: username, which is a string; status of the user which
+ can be one of the following three strings 'new', 'valid' or
+ 'invalid'; and the last login time which is a datetime Python
+ object. Write a function called **query** that takes a filter
+ dictionary as a parameter and returns the result of the items in
+ the *data* list. They keys of the dictionary can be 'user',
+ 'status' and 'logtime' and their corresponding values can be any
+ of the valid values for the corresponding key. Example filter
+ dictionary::
+
+ filter = {
+ 'user': 'john'
+ 'status': 'new'
+ }
+
+ Place your function in a file called query.py. Before writing the
+ actual function, follow the test driven development
+ approach. First write a stub, fail the tests and then write the
+ code and make sure the tests pass. Specifically use unittest
+ framework to test this function. Place your tests in a file
+ called test_query.py
+
+ A developer wrote a small utility function in a file named
+ user_utils.py which uses your **query** function which looks as
+ follows::
+
+ def login_util(user=None):
+ """Takes a user name and returns his last login time if the
+ user is a valid user, else return None. If the user is
+ 'host' it returns the last login time of all the users.
+ """
+
+ filter_dict = {
+ 'user': user
+ 'status': 'active'
+ }
+
+ if user == 'host':
+ filter_dict['status'] + ['new', 'invalid']
+
+ return query(filter_dict)
+
+ Unfortunately the developer did not provide us with the test
+ cases. We wrote the following test cases for you to only discover
+ that the function miserably fails.
+
+ The tests were placed in a file called test_user_utils.py and we
+ have used the unittest framework::
+
+ import query
+ import user_utils
+ import unittest
+
+ class TestUserUtils(unittest.TestCase):
+
+ def setUp(self):
+ """Boiler plate method to provide common data to all
+ the test methods.
+ """
+ self.test_names = ['Alex', 'Guido', 'Thomas', 'host',
+ 'Tom', 'James']
+ self.data_len = len(query.data)
+
+ def test_login_utils(self):
+ """Tests for the login_utils function.
+ """
+
+ for name in self.test_names:
+ if name == 'host':
+ assertEqual(len(user_utils.login_utils(name)), self.data_len)
+ else:
+ assertLess(len(user_utils.login_utils(name)), self.data_len)
+
+ def tearDown(self):
+ """Boiler plate method to clean up all the data created
+ for tests.
+ """
+
+ del self.test_names
+ del self.data_len
+
+ Fix the bug, run the tests to make sure the function passes the
+ tests and if possible refactor the code with a better approach. - An-level
diff --git a/lecture-notes/tdd/math_utils/gcd.py b/lecture-notes/tdd/math_utils/gcd.py
new file mode 100644
index 0000000..7204ac0
--- /dev/null
+++ b/lecture-notes/tdd/math_utils/gcd.py
@@ -0,0 +1,22 @@
+def gcd(a, b):
+ """Returns the Greatest Common Divisor of the two integers
+ passed as arguments.
+
+ Args:
+ a: an integer
+ b: another integer
+
+ Returns: Greatest Common Divisor of a and b
+
+ >>> gcd(48, 64)
+ 16
+ >>> gcd(44, 19)
+ 1
+ """
+ if b == 0:
+ return b
+ return gcd(b, a%b)
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/lecture-notes/tdd/math_utils/gcd_testcases.dat b/lecture-notes/tdd/math_utils/gcd_testcases.dat
new file mode 100644
index 0000000..3829b12
--- /dev/null
+++ b/lecture-notes/tdd/math_utils/gcd_testcases.dat
@@ -0,0 +1,50 @@
+6, 22, 2
+6, 48744, 6
+14, 143295, 1
+22, 751, 1
+35, 79, 1
+35, 96, 1
+52, 12, 4
+73, 79, 1
+73, 184790, 1
+86, 11, 1
+93, 8, 1
+93, 798, 3
+113, 42785, 1
+209, 2135, 1
+395, 8989, 1
+587, 331, 1
+643, 751, 1
+721, 242525, 1
+733, 5622, 1
+854, 42785, 1
+1695, 57, 3
+1695, 798, 3
+3429, 177203, 1
+4603, 12, 1
+4603, 48744, 1
+6139, 57, 1
+6139, 204, 1
+6660, 96, 12
+6660, 410400, 180
+6703, 410400, 1
+8964, 22, 2
+9673, 751, 1
+9673, 7909, 1
+9673, 3335, 1
+16028, 891, 1
+44231, 378, 1
+49020, 751, 1
+57908, 184790, 2
+65482, 548045, 1
+79715, 8, 1
+79715, 891, 1
+79715, 66371, 1
+321807, 891, 3
+366607, 97, 1
+402212, 5595, 1
+448426, 66371, 1
+575271, 4617, 9
+575271, 402152, 1
+680256, 48744, 72
+779565, 184790, 5
diff --git a/lecture-notes/tdd/math_utils/test_gcd.py b/lecture-notes/tdd/math_utils/test_gcd.py
new file mode 100644
index 0000000..c81c72b
--- /dev/null
+++ b/lecture-notes/tdd/math_utils/test_gcd.py
@@ -0,0 +1,29 @@
+import gcd
+import unittest
+
+class TestGcdFunction(unittest.TestCase):
+
+ def setUp(self):
+ self.test_file = open('gcd_testcases.dat')
+ self.test_cases = []
+ for line in self.test_file:
+ values = line.split(', ')
+ a = int(values[0])
+ b = int(values[1])
+ g = int(values[2])
+
+ self.test_cases.append([a, b, g])
+
+ def test_gcd(self):
+ for case in self.test_cases:
+ a = case[0]
+ b = case[1]
+ g = case[2]
+ self.assertEqual(gcd.gcd(a, b), g)
+
+ def tearDown(self):
+ self.test_file.close()
+ del self.test_cases
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/lecture-notes/tdd/tdd.rst b/lecture-notes/tdd/tdd.rst
new file mode 100644
index 0000000..6347c47
--- /dev/null
+++ b/lecture-notes/tdd/tdd.rst
@@ -0,0 +1,656 @@
+=======================
+Test Driven Development
+=======================
+
+What is TDD?
+============
+
+Objectives
+----------
+At the end of this section, you will be able to:
+
+1. Write your code using the TDD paradigm.
+#. Use doctests to test your Python code.
+#. Use unittests to test your Python code.
+#. Use the nose module to test your code.
+
+
+Test Driven Development (TDD), as the name suggests, is a style or method
+of software development based on the idea of writing code, after writing
+tests for them. As the tests are written for code, that doesn't even exist
+yet, it is bound to fail. Actual code is later written, to pass the test
+and later on refactored.
+
+The basic steps of TDD are roughly as follows -
+
+1. Decide upon the feature to implement and the methodology of testing it.
+#. Write the tests for the feature decided upon.
+#. Just write enough code, so that the test can be run, but it fails.
+#. Improve the code, to just pass the test and at the same time passing all
+ previous tests.
+#. Run the tests to see, that all of them run successfully.
+#. Refactor the code we've just written -- optimize the algorithm, remove
+ duplication, add documentation, etc.
+#. Run the tests again, to see that all the tests still pass.
+#. Go back to 1.
+
+First "Test"
+============
+
+Now, that we have an overview of TDD, let's get down to writing our first
+test. Let us consider a very simple program which returns the Greatest
+Common Divisor (GCD) of two numbers.
+
+Before being able to write our test, we need to have a clear idea of the
+code units our program will contain, so that we can test each of them.
+Let's first define the code units that our GCD program is going to have.
+
+Our program is going to contain only one function called ``gcd``, which
+will take in two arguments, and return a single value, which is the GCD of
+the two arguments. So if we want to find out GCD of 44, 23, we call the
+function, with the arguments 44 and 23.
+
+::
+
+ c = gcd(44, 23)
+
+c will contain the GCD of the two numbers.
+
+We have defined the code units in our program. So, we can go ahead and
+write tests for it.
+
+When adopting the TDD methodology, it is important to have a clear set of
+results defined for our code units i.e., for every given set of inputs as
+that we wish use as a test case, we must have, before hand, the exact
+outputs that are expected for those input test cases. We must not be
+running around looking for outputs for our test cases after we have the
+code ready, or even while we are writing the tests.
+
+Let one of our test cases be 48 and 64 as ``a`` and ``b``, respectively.
+For this test case we know that the expected output, the GCD, is 16. Let
+our second test case be 44 and 19 as ``a`` and ``b``, respectively. We know
+that their GCD is 1, and that is the expected output.
+
+But before writing one, we should What does a test look like? What does it
+do? Let's answer this question first.
+
+Tests are just a series of assertions which are either True or False
+depending on the expected behaviour of the code and the actual behaviour.
+Tests for out GCD function could be written as follows.
+
+::
+
+ tc1 = gcd(48, 64)
+ if tc1 != 16:
+ print "Failed for a=48, b=64. Expected 16. Obtained %d instead." % tc1
+ exit(1)
+
+ tc2 = gcd(44, 19)
+ if tc2 != 1:
+ print "Failed for a=44, b=19. Expected 1. Obtained %d instead." % tc2
+ exit(1)
+
+ print "All tests passed!"
+
+The next step is to run the tests we have written, but if we run the tests
+now, the tests wouldn't even run. Why? We don't even have the function
+``gcd`` defined, for it to be called by the tests. The test code doesn't
+even run! What is the way out? We shall first write a very minimal
+definition (or a stub) of the function ``gcd``, so that it can be called by
+the tests.
+
+A minimal definition for the ``gcd`` function would look like this
+
+::
+
+ def gcd(a, b):
+ pass
+
+As you can see, the stub for ``gcd`` function does nothing, other than
+define the required function which takes the two arguments a and b. No
+action is done upon the arguments, passed to the function, yet. The
+function definition is empty and just contains the ``pass`` statement.
+
+Let us put all these in a file and call this file ``gcd.py``
+
+::
+
+ def gcd(a, b):
+ pass
+
+ if __name__ == '__main__':
+ tc1 = gcd(48, 64)
+ if tc1 != 16:
+ print "Failed for a=48 and b=64. Expected 16. Obtained %d instead." % tc1
+ exit(1)
+
+ tc2 = gcd(44, 19)
+ if tc2 != 1:
+ print "Failed for a=44 and b=19. Expected 1. Obtained %d instead." % tc2
+ exit(1)
+
+ print "All tests passed!"
+
+Recall that, the condition ``__name__ == '__main__'`` becomes true, only
+when the python script is ran directly as a stand-alone script. So any code
+within this ``if`` block is executed only when the script is run as a
+stand-alone script and doesn't run when it is used as a module and
+imported.
+
+Let us run our code as a stand-alone script.
+
+::
+
+ $ python gcd.py
+ Traceback (most recent call last):
+ File "gcd.py", line 7, in <module> print "Failed for a=48 and b=64. Expected 16. Obtained %d instead." % tc1
+ TypeError: %d format: a number is required, not NoneType
+
+We now have our tests, the code unit stub, and a failing test. The next
+step is to write code, so that the test just passes.
+
+Let's us the algorithm given by a greek mathematician, 2300 years ago, the
+Euclidean algorithm, to compute the GCD of two numbers.
+
+**Note**: If you are unaware of Euclidean algorithm to compute the gcd of
+two numbers please refer to it on wikipedia. It has a very detailed
+explanation of the algorithm and its proof of validity among other things.
+
+The ``gcd`` stub function can be modified to the following function,
+
+::
+
+ def gcd(a, b):
+ if a == 0:
+ return b
+ while b != 0:
+ if a > b:
+ a = a - b
+ else:
+ b = b - a
+ return a
+
+Now let us run our script which already has the tests written in it and see
+what happens
+
+::
+
+ $ python gcd.py
+ All tests passed!
+
+Success! We managed to write code, that passes all the tests. The code we
+wrote was simplistic, actually. If you take a closer look at the code you
+will soon realize that the chain of subtraction operations can be replaced
+by a modulo operation i.e. taking remainders of the division between the
+two numbers since they are equivalent operations. The modulo operation is
+far better, since it combines a lot of subtractions into one operation and
+the reduction is much faster. Take a few examples, and convince yourself
+that using the modulo operation is indeed faster than the subtraction
+method.
+
+::
+
+ def gcd1(a, b):
+ while b != 0 and a != 0:
+ if a > b:
+ a = a%b
+ else:
+ b = b%a
+ if a == 0:
+ return b
+ else:
+ return a
+
+But we know that the modulo of ``a%b`` is always less than b. So, if the
+condition ``a>b`` is True in this iteration, it will definitely be False in
+the next iteration. Also note that , and in the case when ``a < b``,
+``a%b`` is equal to ``a``. So, we could, essentially, get rid of the
+``if-else`` block and combine a swapping operation along with the modulo
+operation.
+
+::
+
+ def gcd(a, b):
+ while b != 0:
+ a, b = b, a % b
+ return a
+
+Let's run our tests again, and check that all the tests are passed, again.
+
+We could make one final
+improvement to our ``gcd`` function, which is strictly not necessary in
+terms of efficiency, but is definitely more readable and easy to
+understand. We could make our function a recursive function, as shown below
+
+::
+
+ def gcd(a, b):
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+
+Much shorter and sweeter! We again run all the tests, and check that they
+are passed. It indeed does pass all the tests!
+
+But there is still one small problem. There is no way, for the users of
+this function, to determine how to use it, how many arguments it takes and
+what it returns, among other things. This is a handicap for the people
+reading your code, as well. This is well written function, with no
+documentation whatsoever.
+
+Let's add a docstring as shown,
+
+::
+
+ def gcd(a, b):
+ """Returns the Greatest Common Divisor of the two integers
+ passed as arguments.
+
+ Args:
+ a: an integer
+ b: another integer
+
+ Returns: Greatest Common Divisor of a and b
+ """
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+
+Now we have refactored our code enough to make it well written piece
+of code. We have now successfully completed writing our first test, writing
+the relevant code, ensuring that the tests are passed and refactoring the
+code, until we are happy with the way it works and looks.
+
+Let's now build on what we have learnt so far and learn more about writing
+tests and improve our methodology of writing tests and simplify the process
+of writing them.
+
+Persistent Test Cases
+---------------------
+
+As already stated, tests should be predetermined and you should have your
+test cases and their outputs ready, even before you write the first line of
+code. This test data is repeatedly used in the process of writing code. So,
+it makes sense to have the test data to be stored in some persistent format
+like a database, a text file, a file of specific format like XML, etc.
+
+Let us modify the test code for the GCD function and save our test data in
+a text file. Let us decide upon the following format for the test data.
+
+ 1. The file has multiple lines of test data.
+ 2. Each line in this file corresponds to a single test case.
+ 3. Each line consists of three comma separated values --
+
+ i. First two coloumns are the integers for which the GCD has to
+ be computed
+ ii. Third coloumn is the expected GCD to the preceding two
+ numbers.
+
+Now, how do we modify our tests to use this test data? We read the file
+line by line and parse it to obtain the required numbers whose GCD we wish
+to calculate, and the expected output. We can now call the ``gcd`` function
+with the correct arguments and verify the output with the expected output.
+
+Let us call our data file ``gcd_testcases.dat``
+
+::
+
+ if __name__ == '__main__':
+ for line in open('gcd_testcases.dat'):
+ values = line.split(', ')
+ a = int(values[0])
+ b = int(values[1])
+ g = int(values[2])
+
+ tc = gcd(a, b)
+ if tc != g:
+ print "Failed for a=%d and b=%d. Expected %d. Obtained %d instead." % (a, b, g, tc)
+ exit(1)
+
+ print "All tests passed!"
+
+When we execute the gcd.py script again we will notice that the code passes
+all the tests.
+
+Now, we have learnt how to write simple test cases and develop code based
+on these test cases. We shall now move on to learn to use some testing
+frameworks available for Python, which make some of the task easier.
+
+Python Testing Frameworks
+=========================
+
+Python provides two ways to test the code we have written. One of them is
+the unittest framework and the the other is the doctest module. To start
+with, let us discuss the doctest module.
+
+doctest
+~~~~~~~
+
+As we have already discussed, a well written piece of code must always be
+accompanied by documentation. We document functions or modules using
+docstrings. In addition to writing a generic description of the function or
+the module, we can also add examples of using these functions in the
+interactive interpreter to the docstrings.
+
+Using the ``doctest`` module, we can pick up all such interactive session
+examples, execute them, and determine if the documented piece of code runs,
+as it was documented.
+
+The example below shows how to write ``doctests`` for our ``gcd`` function.
+
+::
+
+ def gcd(a, b):
+ """Returns the Greatest Common Divisor of the two integers
+ passed as arguments.
+
+ Args:
+ a: an integer
+ b: another integer
+
+ Returns: Greatest Common Divisor of a and b
+
+ >>> gcd(48, 64)
+ 16
+ >>> gcd(44, 19)
+ 1
+ """
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+
+That is all there is, to writing a ``doctest`` in Python. Now how do we use
+the ``doctest`` module to execute these tests. That too, is fairly straight
+forward. All we need to do is tell the doctest module to execute, using the
+``doctest.testmod`` function.
+
+Let us place this piece of code at the same place where we placed our tests
+earlier. So putting all these together we have our ``gcd.py`` module which
+looks as follows
+
+::
+
+ def gcd(a, b):
+ """Returns the Greatest Common Divisor of the two integers
+ passed as arguments.
+
+ Args:
+ a: an integer
+ b: another integer
+
+ Returns: Greatest Common Divisor of a and b
+
+ >>> gcd(48, 64)
+ 16
+ >>> gcd(44, 19)
+ 1
+ """
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+
+ if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
+The ``testmod`` function automatically picks up all the docstrings that
+have sample sessions from the interactive interpreter, executes them and
+compares the output with the results as specified in the sample sessions.
+If all the outputs match the expected outputs that have been documented, it
+doesn't say anything. It complains only if the results don't match as
+documented, expected results.
+
+When we execute this script as a stand-alone script we will get back the
+prompt with no messages which means all the tests passed
+
+::
+
+ $ python gcd.py
+ $
+
+If we further want to get a more detailed report of the tests that were
+executed we can run python with -v as the command line option to the script
+
+::
+
+ $ python gcd.py -v
+ Trying:
+ gcd(48, 64)
+ Expecting:
+ 16
+ ok
+ Trying:
+ gcd(44, 19)
+ Expecting:
+ 1
+ ok
+ 1 items had no tests:
+ __main__
+ 1 items passed all tests:
+ 2 tests in __main__.gcd
+ 2 tests in 2 items.
+ 2 passed and 0 failed.
+ Test passed.
+
+
+**Note:** We can have the sample sessions as test cases as long as the
+outputs of the test cases do not contain any blank lines. In such cases we
+may have to use the exact string ``<BLANKLINE>``
+
+For the sake of illustrating a failing test case, let us assume that we
+made a small mistake in our code. Instead of returning ``a`` when b = 0, we
+typed it as ``return b`` when b = 0. Now, all the GCDs returned will have
+the value 0. The code looks as follows
+
+::
+
+ def gcd(a, b):
+ """Returns the Greatest Common Divisor of the two integers
+ passed as arguments.
+
+ Args:
+ a: an integer
+ b: another integer
+
+ Returns: Greatest Common Divisor of a and b
+
+ >>> gcd(48, 64)
+ 16
+ >>> gcd(44, 19)
+ 1
+ """
+ if b == 0:
+ return b
+ return gcd(b, a%b)
+
+Executing this code snippet without -v option to the script
+
+::
+
+ $ python gcd.py
+ **********************************************************************
+ File "gcd.py", line 11, in __main__.gcd
+ Failed example:
+ gcd(48, 64)
+ Expected:
+ 16
+ Got:
+ 0
+ **********************************************************************
+ File "gcd.py", line 13, in __main__.gcd
+ Failed example:
+ gcd(44, 19)
+ Expected:
+ 1
+ Got:
+ 0
+ **********************************************************************
+ 1 items had failures:
+ 2 of 2 in __main__.gcd
+ ***Test Failed*** 2 failures.
+
+The output clearly complains that there were exactly two test cases
+that failed. If we want a more verbose report we can pass -v option to
+the script.
+
+This is all there is, to using the ``doctest`` module in Python.
+``doctest`` is extremely useful when we want to test each Python function
+or module individually.
+
+For more information about the doctest module refer to the Python library
+reference on doctest[0].
+
+unittest framework
+~~~~~~~~~~~~~~~~~~
+
+We needn't go too far ahead, to start complaining that ``doctest`` is not
+sufficient to write complicated tests especially when we want to automate
+our tests, write tests that need to test for more convoluted code pieces.
+For such scenarios, Python provides a ``unittest`` framework.
+
+The ``unittest`` framework provides methods to efficiently automate tests,
+easily initialize code and data for executing the specific tests, cleanly
+shut them down once the tests are executed and easy ways of aggregating
+tests into collections and better way of reporting the tests.
+
+Let us continue testing our gcd function in the Python module named
+``gcd.py``. The ``unittest`` framework expects us to subclass the
+``TestCase`` class in ``unittest`` module and place all our test code as
+methods of this class. The name of the test methods are expected to be
+started with ``test_``, so that the test runner knows which methods are to
+be executed as tests. We shall use the test cases supplied by
+``gcd_testcases.dat``.
+
+Lastly, to illustrate the way to test Python code as a module, let's create
+a new file called ``test_gcd.py`` following the same convention used to
+name the test methods, and place our test code in it.
+
+::
+
+ import gcd
+ import unittest
+
+ class TestGcdFunction(unittest.TestCase):
+
+ def setUp(self):
+ self.test_file = open('gcd_testcases.dat')
+ self.test_cases = []
+ for line in self.test_file:
+ values = line.split(', ')
+ a = int(values[0])
+ b = int(values[1])
+ g = int(values[2])
+
+ self.test_cases.append([a, b, g])
+
+ def test_gcd(self):
+ for case in self.test_cases:
+ a = case[0]
+ b = case[1]
+ g = case[2]
+ self.assertEqual(gcd.gcd(a, b), g)
+
+ def tearDown(self):
+ self.test_file.close()
+ del self.test_cases
+
+ if __name__ == '__main__':
+ unittest.main()
+
+Please note that although we highly recommend writing docstrings for all
+the classes, functions and modules, we have not done so, in this example to
+keep it compact. Adding suitable docstrings wherever required is left to
+you, as an exercise.
+
+It would be a waste, to read the test data file, each time we run a test
+method. So, in the setUp method, we read all the test data and store it
+into a list called test_cases, which is an attribute of the TestGCDFunction
+class. In the tearDown method of the class we will delete this attribute to
+free up the memory and close the opened file.
+
+Our actual test code sits in the method which begins with the name
+``test_``, as stated earlier, the ``test_gcd`` method. Note that, we import
+the ``gcd`` Python module we have written at the top of this test file and
+from this test method we call the ``gcd`` function within the ``gcd``
+module to be tested with the each set of ``a`` and ``b`` values
+``test_cases``. Once we execute the ``test_gcd`` function, we obtain the
+result and compare it with the expected result as stored in the
+corresponding ``test_cases`` attribute using the ``assertEqual`` method
+provided by ``TestCase`` class in the ``unittest`` framework. There are
+several other assertion methods supplied by the unittest framework. For a
+more detailed information about this, refer to the unittest library
+reference at [1]. This brings us to the end of our discussion of the
+``unittest`` framework.
+
+nose
+====
+
+We have seen the ``unittest`` frame work and Python ``doctest`` module. .
+One problem however remains. It is not easy to organize, choose and run
+tests, when our code is scattered across multiple files. In the real world
+scenario, this is often the case.
+
+In such a such a scenario, a tool which can aggregate these tests
+automatically, and run them. The ``nose`` module, does precisely this, for
+us. It can aggregate ``unittests`` and ``doctests`` into a collection and
+run them. It also helps output the test-results and aggregate them in
+various formats.
+
+Although nose is not part of the standard Python distribution itself, it
+can be very easily installed by using easy_install command as follows
+
+::
+
+ $ easy_install nose
+
+Or download the nose package from [2], extracting the archive and running
+the command from the extracted directory
+
+::
+
+ $ python setup.py install
+
+Now we have nose up and running, but how do we use it? We shall use the
+``nosetests`` command provided by the ``nose`` module, in the top-level
+directory of our code.
+
+::
+
+ $ nosetests
+
+That's all! ``nose`` will now automatically pick all the tests in all the
+directories and subdirectories in our code base and execute them.
+
+However if we want to execute specific tests we can pass the test file
+names or the directories as arguments to nosetests command. For a detailed
+explanation about this, refer to [3]
+
+Conclusion
+==========
+
+Now we have all the trappings we want to write state-of-the art tests. To
+emphasize the same point again, any code which was written before writing
+the test and the testcases in hand is flawed by design. So it is
+recommended to follow the three step approach while writing code for any
+project as below:
+
+ 1. Write failing tests with testcases in hand.
+ 2. Write the code to pass the tests.
+ 3. Refactor the code for better performance.
+
+This approach is very famously known to the software development world as
+"Red-Green-Refactor" approach[4].
+
+[0] - http://docs.python.org/library/doctest.html
+[1] - http://docs.python.org/library/unittest.html
+[2] - http://pypi.python.org/pypi/nose/
+[3] - http://somethingaboutorange.com/mrl/projects/nose/0.11.2/usage.html
+[4] - http://en.wikipedia.org/wiki/Test-driven_development
+
+..
+ Local Variables:
+ mode: rst
+ indent-tabs-mode: nil
+ sentence-end-double-space: nil
+ fill-column: 75
+ End:
diff --git a/lecture-notes/tdd/tdd.tex b/lecture-notes/tdd/tdd.tex
new file mode 100644
index 0000000..1d64be2
--- /dev/null
+++ b/lecture-notes/tdd/tdd.tex
@@ -0,0 +1,514 @@
+\documentclass[12pt,presentation]{beamer}
+\usepackage[utf8]{inputenc}
+\usepackage[T1]{fontenc}
+\usepackage{fixltx2e}
+\usepackage{graphicx}
+\usepackage{longtable}
+\usepackage{float}
+\usepackage{wrapfig}
+\usepackage{soul}
+\usepackage{textcomp}
+\usepackage{marvosym}
+\usepackage{wasysym}
+\usepackage{latexsym}
+\usepackage{amssymb}
+\usepackage{hyperref}
+\tolerance=1000
+\usepackage[english]{babel} \usepackage{ae,aecompl}
+\usepackage{mathpazo,courier,euler} \usepackage[scaled=.95]{helvet}
+\usepackage{listings}
+\lstset{language=Python, basicstyle=\ttfamily\bfseries,
+commentstyle=\color{red}\itshape, stringstyle=\color{green},
+showstringspaces=false, keywordstyle=\color{blue}\bfseries}
+\providecommand{\alert}[1]{\textbf{#1}}
+
+\title{SEES: Test Driven Development}
+\author{FOSSEE}
+
+\usetheme{Warsaw}\usecolortheme{default}\useoutertheme{infolines}\setbeamercovered{transparent}
+
+\AtBeginSection[]
+{
+ \begin{frame}<beamer>
+ \frametitle{Outline}
+ \tableofcontents[currentsection]
+ \end{frame}
+}
+
+\begin{document}
+
+\maketitle
+
+\begin{frame}
+\frametitle{Outline}
+\setcounter{tocdepth}{3}
+\tableofcontents
+\end{frame}
+
+\section{Introduction}
+
+\begin{frame}
+ \frametitle{Objectives}
+ At the end of this section, you will be able to:
+ \begin{itemize}
+ \item Write your code using the TDD paradigm.
+ \item Use doctests to test your Python code.
+ \item Use unittests to test your Python code.
+ \item Use the nose module to test your code.
+ \end{itemize}
+\end{frame}
+
+\begin{frame}
+ \frametitle{What is TDD?}
+ The basic steps of TDD are roughly as follows --
+ \begin{enumerate}
+ \item Decide upon the feature to implement and the methodology of
+ testing it.
+ \item Write the tests for the feature decided upon.
+ \item Just write enough code, so that the test can be run, but it fails.
+ \item Improve the code, to just pass the test and at the same time
+ passing all previous tests.
+ \item Run the tests to see, that all of them run successfully.
+ \item Refactor the code you've just written -- optimize the algorithm,
+ remove duplication, add documentation, etc.
+ \item Run the tests again, to see that all the tests still pass.
+ \item Go back to 1.
+ \end{enumerate}
+\end{frame}
+
+\section{First Test}
+
+\begin{frame}[fragile]
+ \frametitle{First Test -- GCD}
+ \begin{itemize}
+ \item simple program -- GCD of two numbers
+ \item What are our code units?
+ \begin{itemize}
+ \item Only one function \texttt{gcd}
+ \item Takes two numbers as arguments
+ \item Returns one number, which is their GCD
+ \end{itemize}
+\begin{lstlisting}
+c = gcd(44, 23)
+\end{lstlisting}
+ \item c will contain the GCD of the two numbers.
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{Test Cases}
+ \begin{itemize}
+ \item Important to have test cases and expected outputs even before
+ writing the first test!
+ \item $a=48$, $b=48$, $GCD=48$
+ \item $a=44$, $b=19$, $GCD=1$
+ \item Tests are just a series of assertions
+ \item True or False, depending on expected and actual behavior
+ \end{itemize}
+
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{Test Cases -- Code}
+\begin{lstlisting}
+tc1 = gcd(48, 64)
+if tc1 != 16:
+ print "Failed for a=48, b=64. Expected 16. \
+ Obtained %d instead." % tc1
+ exit(1)
+
+tc2 = gcd(44, 19)
+if tc2 != 1:
+ print "Failed for a=44, b=19. Expected 1. \
+ Obtained %d instead." % tc2
+ exit(1)
+
+print "All tests passed!"
+\end{lstlisting}
+\begin{itemize}
+\item The function \texttt{gcd} doesn't even exist!
+\end{itemize}
+\end{frame}
+
+
+\begin{frame}[fragile]
+ \frametitle{Stubs}
+ \begin{itemize}
+ \item First write a very minimal definition of \texttt{gcd}
+ \begin{lstlisting}
+def gcd(a, b):
+ pass
+ \end{lstlisting}
+ \item Written just, so that the tests can run
+ \item Obviously, the tests are going to fail
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{\texttt{gcd.py}}
+\begin{lstlisting}
+def gcd(a, b):
+ pass
+
+if __name__ == '__main__':
+ tc1 = gcd(48, 64)
+ if tc1 != 16:
+ print "Failed for a=48 and b=64. \
+ Expected 16. Obtained %d instead." % tc1
+ exit(1)
+ tc2 = gcd(44, 19)
+ if tc2 != 1:
+ print "Failed for a=44 and b=19. \
+ Expected 1. Obtained %d instead." % tc2
+ exit(1)
+ print "All tests passed!"
+\end{lstlisting}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{First run}
+\begin{lstlisting}
+$ python gcd.py
+Traceback (most recent call last):
+ File "gcd.py", line 7, in <module>
+ print "Failed for a=48 and b=64. Expected 16.
+ Obtained %d instead." % tc1
+TypeError: %d format:
+a number is required, not NoneType
+\end{lstlisting} %$
+
+ \begin{itemize}
+ \item We have our code unit stub, and a failing test.
+ \item The next step is to write code, so that the test just passes.
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{Euclidean Algorithm}
+ \begin{itemize}
+ \item Modify the \texttt{gcd} stub function
+ \item Then, run the script to see if the tests pass.
+ \end{itemize}
+\begin{lstlisting}
+def gcd(a, b):
+ if a == 0:
+ return b
+ while b != 0:
+ if a > b:
+ a = a - b
+ else:
+ b = b - a
+ return a
+\end{lstlisting}
+\begin{lstlisting}
+$ python gcd.py
+All tests passed!
+\end{lstlisting} %$
+ \begin{itemize}
+ \item \alert{Success!}
+ \end{itemize}
+\end{frame}
+
+
+\begin{frame}[fragile]
+ \frametitle{Euclidean Algorithm -- Modulo}
+ \begin{itemize}
+ \item Repeated subtraction can be replaced by a modulo
+ \item modulo of \texttt{a\%b} is always less than b
+ \item when \texttt{a < b}, \texttt{a\%b} equals \texttt{a}
+ \item Combine these two observations, and modify the code
+\begin{lstlisting}
+def gcd(a, b):
+ while b != 0:
+ a, b = b, a % b
+ return a
+\end{lstlisting}
+ \item Check that the tests pass again
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{Euclidean Algorithm -- Recursive}
+ \begin{itemize}
+ \item Final improvement -- make \texttt{gcd} recursive
+ \item More readable and easier to understand
+\begin{lstlisting}
+def gcd(a, b):
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+\end{lstlisting}
+ \item Check that the tests pass again
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{Document \texttt{gcd}}
+ \begin{itemize}
+ \item Undocumented function is as good as unusable
+ \item Let's add a docstring \& We have our first test!
+ \end{itemize}
+\begin{lstlisting}
+def gcd(a, b):
+ """Returns the Greatest Common Divisor of the
+ two integers passed as arguments.
+
+ Args:
+ a: an integer
+ b: another integer
+
+ Returns: Greatest Common Divisor of a and b
+ """
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+\end{lstlisting}
+\end{frame}
+
+
+\begin{frame}[fragile]
+ \frametitle{Persistent Test Cases}
+ \begin{itemize}
+ \item Tests should be pre-determined and written, before the code
+ \item Test Data is repeatedly used; Hence, saved in persistent
+ format
+ \item Let's save data for GCD tests in a text file.
+ \item The file shall have multiple lines of test data
+ \item Each line corresponds to a single test case
+ \item Each line consists of three comma separated values --
+ \begin{itemize}
+ \item First two coloumns are the integers for which the GCD has to be
+ computed
+ \item Third coloumn is the expected GCD to the preceding two
+ numbers.
+ \end{itemize}
+ \item Let us call our data file \texttt{gcd\_testcases.dat}
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{Modify \texttt{gcd.py}}
+\begin{lstlisting}
+if __name__ == '__main__':
+ for line in open('gcd_testcases.dat'):
+ values = line.split(', ')
+ a = int(values[0])
+ b = int(values[1])
+ g = int(values[2])
+
+ tc = gcd(a, b)
+ if tc != g:
+ print "Failed for a=%d and b=%d.\
+ Expected %d. Obtained %d instead."\
+ % (a, b, g, tc)
+ exit(1)
+
+ print "All tests passed!"
+\end{lstlisting}
+\end{frame}
+
+\section{Python Testing Frameworks}
+
+\begin{frame}[fragile]
+ \frametitle{Python Testing Frameworks}
+ \begin{itemize}
+ \item Testing frameworks essentially, ease the job of the user
+ \item Python provides two frameworks for testing code
+ \begin{itemize}
+ \item \texttt{unittest} framework
+ \item \texttt{doctest} module
+ \end{itemize}
+ \end{itemize}
+\end{frame}
+
+\subsection{\texttt{doctest} module}
+
+\begin{frame}[fragile]
+ \frametitle{doctest}
+ \begin{itemize}
+ \item Documentation always accompanies a well written piece of code
+ \item Use \texttt{docstring} to document functions or modules
+ \item Along with description and usage, examples can be added
+ \item Interactive interpreter session inputs and outputs are
+ copy-pasted
+ \item \texttt{doctest} module picks up all such interactive examples
+ \item Executes them and determines if the code runs, as documented
+ \end{itemize}
+ Let's use the \texttt{doctest} module for our \texttt{gcd} function
+\end{frame}
+
+
+\begin{frame}[fragile]
+ \frametitle{doctest for \texttt{gcd.py}}
+\begin{tiny}
+\begin{lstlisting}
+def gcd(a, b):
+ """Returns the Greatest Common Divisor of the two integers
+ passed as arguments.
+
+ Args:
+ a: an integer
+ b: another integer
+
+ Returns: Greatest Common Divisor of a and b
+
+ >>> gcd(48, 64)
+ 16
+ >>> gcd(44, 19)
+ 1
+ """
+ if b == 0:
+ return a
+ return gcd(b, a%b)
+\end{lstlisting}
+\end{tiny}
+\begin{itemize}
+\item We have added examples to the \texttt{docstring}
+\item Now we need to tell the \texttt{doctest} module to execute
+\end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{doctest for \texttt{gcd.py} \ldots}
+\begin{lstlisting}
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+\end{lstlisting}
+\begin{itemize}
+\item \texttt{testmod} automatically picks all sample sessions
+\item Executes them and compares output with documented output
+\item It doesn't give any output, when all the results match
+\item Complains only when one or more tests fail.
+\end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{\texttt{doctest} -- Execution}
+ \begin{itemize}
+ \item Run the doctests by running \texttt{gcd.py}
+\begin{lstlisting}
+$ python gcd.py
+\end{lstlisting} %$
+ \item All the tests pass, and doctest gives no output
+ \item For a more detailed report we can run with -v argument
+\begin{lstlisting}
+$ python gcd.py -v
+\end{lstlisting} %$
+ \item If the output contains a blank line, use \texttt{<BLANKLINE>}
+ \item To see a failing test case, replace \texttt{return a} with \texttt{b}
+ \end{itemize}
+\end{frame}
+
+\subsection{\texttt{unittest} framework}
+
+\begin{frame}[fragile]
+ \frametitle{\texttt{unittest}}
+ \begin{itemize}
+ \item It won't be long, before we complain about the power of
+ \texttt{doctest}
+ \item \texttt{unittest} framework can efficiently automate tests
+ \item Easily initialize code and data for executing the specific
+ tests
+ \item Cleanly shut them down once the tests are executed
+ \item Easily aggregate tests into collections and improved reporting
+ \end{itemize}
+\end{frame}
+
+
+\begin{frame}[fragile]
+ \frametitle{\texttt{unittest}}
+ \begin{itemize}
+ \item It won't be long, before we complain about the power of
+ \texttt{doctest}
+ \item \texttt{unittest} framework can efficiently automate tests
+ \item Easily initialize code and data for executing the specific
+ tests
+ \item Cleanly shut them down once the tests are executed
+ \item Easily aggregate tests into collections and improved reporting
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{\texttt{unittest}ing \texttt{gcd.py}}
+ \begin{itemize}
+ \item Subclass the \texttt{TestCase} class in \texttt{unittest}
+ \item Place all the test code as methods of this class
+ \item Use the test cases present in \texttt{gcd\_testcases.dat}
+ \item Place the code in \texttt{test\_gcd.py}
+ \end{itemize}
+\end{frame}
+
+\begin{frame}[fragile,allowframebreaks]
+ \frametitle{\texttt{test\_gcd.py}}
+\small
+\begin{lstlisting}
+import gcd
+import unittest
+
+class TestGcdFunction(unittest.TestCase):
+
+ def setUp(self):
+ self.test_file = open('gcd_testcases.dat')
+ self.test_cases = []
+ for line in self.test_file:
+ values = line.split(', ')
+ a = int(values[0])
+ b = int(values[1])
+ g = int(values[2])
+
+ self.test_cases.append([a, b, g])
+
+ def test_gcd(self):
+ for case in self.test_cases:
+ a = case[0]
+ b = case[1]
+ g = case[2]
+ self.assertEqual(gcd.gcd(a, b), g)
+
+ def tearDown(self):
+ self.test_file.close()
+ del self.test_cases
+
+if __name__ == '__main__':
+ unittest.main()
+\end{lstlisting}
+\end{frame}
+
+\begin{frame}[fragile]
+ \frametitle{\texttt{test\_gcd.py}}
+ \begin{itemize}
+ \item \texttt{setUp} -- we read all the test data and store it in a
+ list
+ \item \texttt{tearDown} -- delete the data to free up memory and
+ close open file
+ \item \texttt{test\_gcd} -- actual test code
+ \item \texttt{assertEqual} -- compare actual result with expected one
+ \item Write documentation for above code.
+ \end{itemize}
+\end{frame}
+
+\section{\texttt{nose}}
+
+\begin{frame}[fragile]
+ \frametitle{\texttt{nose} tests}
+ \begin{itemize}
+ \item It is not easy to organize, choose and run tests scattered
+ across multiple files.
+ \item \texttt{nose} module aggregate these tests automatically
+ \item Can aggregate \texttt{unittests} and \texttt{doctests}
+ \item Allows us to pick and choose which tests to run
+ \item Helps output the test-results and aggregate them in various
+ formats
+ \item Not part of the Python distribution itself
+\begin{lstlisting}
+$ easy_install nose
+\end{lstlisting} %$
+ \item Run the following command in the top level directory
+\begin{lstlisting}
+$ nosetests
+\end{lstlisting} %$
+ \end{itemize}
+\end{frame}
+
+\end{document}