Replaced complement with antiparallel example
authorMike Jackson <michaelj@epcc.ed.ac.uk>
Thu, 4 Apr 2013 13:58:55 +0000 (06:58 -0700)
committerW. Trevor King <wking@tremily.us>
Fri, 1 Nov 2013 15:56:16 +0000 (08:56 -0700)
testing/TDD.md
testing/python/exercise/cdna.py [deleted file]
testing/python/exercise/dnautils.py [new file with mode: 0755]
testing/python/exercise/test_cdna.py [deleted file]
testing/python/exercise/test_dnautils.py [new file with mode: 0755]

index 4b0bd2667fe9a9354aa4bd421ac89fd61ba84a51..b12515a45e0f2079033e4818fb9bd2b69adb2c06 100755 (executable)
@@ -1,61 +1,73 @@
 ## Test-driven development
 
-Traditionally, we'd write our code, then write the tests. [Test driven development](http://www.amazon.com/Test-Driven-Development-By-Example/dp/0321146530) (TDD), proposed by Kent Beck, is a philosophy that turns this on its head - we write code by *writing the tests first*, then write the code to make the tests pass. If a new feature is needed, another test is written and the code is expanded to meet this new use case. This continues until the code does what is needed. This can be summarised as red-green-refactor:
+Given a DNA sequence consisting of A, C, T and G, we can create its *complement*, cDNA, by applying a mapping to each nucleotide in turn,
+
+* A => T
+* C => G
+* T => A
+* G => C
+
+For example, given DNA strand GTCA, the cDNA is CAGT. 
+
+We can then create its *antiparallel* by calculating the *inverse* of the sequence, by reversing it. So, the anti-parallel of GTCA is TGAC.
+
+Let's write a function to calculate this antiparallel. Now, before, we had our code and wrote some tests. This time we're going to turn this on it's head and try some test-driven development.
+
+[Test driven development](http://www.amazon.com/Test-Driven-Development-By-Example/dp/0321146530) (TDD), proposed by Kent Beck, is a philosophy and approach where we write code by *writing the tests first*, then write the code to make the tests pass. If a new feature is needed, another test is written and the code is expanded to meet this new use case. This continues until the code does what is needed. This can be summarised as red-green-refactor:
 
  * Red - write tests based on requirements. They fail as there is no code!
  * Green - write/modify code to get tests to pass.
  * Refactor code - clean it up.
 
-By writing tests first, we're forced to think about what our code should do. In contrast, in writing our code then tests, we risk testing what the code actually does, rather than what it should do.
+By writing tests first, we're forced to think about what our code should do. In contrast, in writing our code then tests, we risk testing what the code actually *does*, rather than what it *should* do.
 
 TDD operates on the YAGNI principle (You Ain't Gonna Need It) to avoid developing code for which there is no need.
 
-## TDD of a DNA complement function
+So, back to our example. We'll start by creating a file `test_dnautils.py` and import our function,
 
-Given a DNA sequence consisting of A, C, T and G, we can create its complementary DNA, cDNA, by applying a mapping to each nucleotide in turn,
+    from dnautils import antiparallel
 
-* A => T
-* C => G
-* T => A
-* G => C
+And then run `nosetests`,
 
-For example, given DNA strand GTCA, the cDNA is CAGT. 
+    $ nosetests test_dnautils.py
 
-So, let's write a `complement` function that creates the cDNA strand, given a DNA strand in a string. We'll use TDD, so to start, let's create a file `test_cdna.py` and add a test,
+This fails as not only are there no tests, there's no module or function. Let's create a file, `dnautils.py`, and add a function that does nothing,
 
-    from cdna import complement
+    def antiparallel(sequence):
+        """
+        Calculate the antiparallel of a DNA sequence.
+        @param sequence: a DNA sequence expressed as an upper-case string.
+        @return antiparallel as an upper-class string. 
+        """
+        pass
 
-    def test_complement_a():
-        assert_equals complement('A') == 'T'
+And let's run the tests to date,
 
-And let's run the test,
+    $ nosetests test_dnautils.py
 
-    $ nosetests test_cdna.py
+Zero tests, as we expected! Nnow we need to add some tests. Someone propose a test...
 
-Which fails as we have no function! So, let's create a file `cdna.py`. Our initial function to get the tests to pass could be,
+OK we'll add that test...
 
-    def complement(sequence):
-        return 'T'
+And let's run the tests to date,
 
-This is simplistic, but the test passes. Now let's add another test,
+    $ nosetests test_dnautils.py
 
-    def test_complement_c():
-        assert complement('C') == 'G'
+This fails as our function does nothing. So let's change it to pass...
 
-To get both our tests to pass, we can change our function to be,
+Now let's add another test...someone give me one...
 
-    def complement(sequence):
-        if (sequence == 'A'):
-            return 'T'
-        else:
-            return 'G'
+To get both our tests to pass, we can change our function to be...
 
-Now, add some more tests. Don't worry about `complement` just now.
+Now, add some more tests to `test_dnautils.py` but do not make any more changes to `dnautils.py` or your function, yet.
 
-Let's discuss the tests you've come up with.
+Let's discuss the tests you've come up with...
 
-Now update `complement` to make your tests pass. You may want to reuse some of the logic of `calculate_weight`!
+Now update `antiparallel` to make your tests pass...
 
 When we're done, not only do we have a working function, we also have a set of tests. There's no risk of us leaving the tests "till later" and then never having time to write them.
 
+We now may want to spend time refactoring our function to clean up our code. We can do this with the security of our tests which allow us to detect if any changes we make introduce a bug.
+
 Previous: [Testing in practice](RealWorld.md) Next: [Conclusions and further information](Conclusion.md)
diff --git a/testing/python/exercise/cdna.py b/testing/python/exercise/cdna.py
deleted file mode 100755 (executable)
index a1f7e03..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-COMPLEMENTS = {'A':'T', 'T':'A', 'C':'G', 'G':'C'}
-
-def complement(sequence):
-    """
-    Calculate the complementary sequence of a DNA sequence.
-
-    @param sequence: DNA sequence expressed as a lower-case string.
-    @return complementary sequence.
-    """
-    cdna = ''
-    try:
-        for ch in sequence:
-            cdna += COMPLEMENTS[ch]
-        return cdna
-    except TypeError:
-        raise ValueError('The input is not a sequence e.g. a string or list')
-    except KeyError:
-        raise ValueError('The input is not a sequence of G,T,C,A')
diff --git a/testing/python/exercise/dnautils.py b/testing/python/exercise/dnautils.py
new file mode 100755 (executable)
index 0000000..01cd5db
--- /dev/null
@@ -0,0 +1,38 @@
+COMPLEMENTS = {'A':'T', 'T':'A', 'C':'G', 'G':'C'}
+
+def complement(sequence):
+    """
+    Calculate the complementary sequence of a DNA sequence.
+
+    @param sequence: DNA sequence expressed as a lower-case string.
+    @return complementary sequence.
+    """
+    cdna = ''
+    try:
+        for ch in sequence:
+            cdna += COMPLEMENTS[ch]
+        return cdna
+    except TypeError:
+        raise ValueError('The input is not a sequence e.g. a string or list')
+    except KeyError:
+        raise ValueError('The input is not a sequence of G,T,C,A')
+
+def inverse(sequence):
+    """
+    Calculate the inverse of a DNA sequence.
+
+    @param sequence: a DNA sequence expressed as an upper-case string.
+    @return inverse as an upper-class string. 
+    """
+    # Reverse string using approach recommended on StackOverflow
+    # http://stackoverflow.com/questions/931092/reverse-a-string-in-python
+    return sequence[::-1]
+
+def antiparallel(sequence):
+    """
+    Calculate the antiparallel of a DNA sequence.
+
+    @param sequence: a DNA sequence expressed as an upper-case string.
+    @return antiparallel as an upper-class string. 
+    """
+    return inverse(complement(sequence))
diff --git a/testing/python/exercise/test_cdna.py b/testing/python/exercise/test_cdna.py
deleted file mode 100755 (executable)
index ddfc1e9..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-from cdna import complement
-
-def test_complement_a():
-    assert complement('A') == 'T'
-    
-def test_complement_c():
-    assert complement('C') == 'G'
-
-def test_complement_t():
-    assert complement('T') == 'A'
-
-def test_complement_g():
-    assert complement('G') == 'C'
-
-def test_complement_gatc():
-    assert complement('GATC') == 'CTAG'
-
-def test_complement_empty_string():
-    assert complement('') == ''
diff --git a/testing/python/exercise/test_dnautils.py b/testing/python/exercise/test_dnautils.py
new file mode 100755 (executable)
index 0000000..5bccb3c
--- /dev/null
@@ -0,0 +1,26 @@
+from dnautils import antiparallel
+from nose.tools import assert_raises
+
+def test_antiparallel_a():
+    assert antiparallel('A') == 'T'
+    
+def test_antiparallel_c():
+    assert antiparallel('C') == 'G'
+
+def test_antiparallel_t():
+    assert antiparallel('T') == 'A'
+
+def test_antiparallel_g():
+    assert antiparallel('G') == 'C'
+
+def test_antiparallel_gggg():
+    assert antiparallel('GGGG') == 'CCCC'
+
+def test_antiparallel_gtca():
+    assert antiparallel('GTCA') == 'TGAC'
+
+def test_antiparallel_empty_string():
+    assert antiparallel('') == ''
+
+def test_123():
+    assert_raises(ValueError, antiparallel, 123)