Replaced DNA with morse example!
authorMike Jackson <michaelj@epcc.ed.ac.uk>
Wed, 3 Jul 2013 12:00:54 +0000 (13:00 +0100)
committerW. Trevor King <wking@tremily.us>
Fri, 1 Nov 2013 20:21:50 +0000 (13:21 -0700)
testing/README.md
testing/python/dna/dna.py [deleted file]
testing/python/exercise/dnautils.py [deleted file]
testing/python/exercise/test_dnautils.py [deleted file]
testing/python/morse.py [new file with mode: 0644]

index d577306cd8e141d42bd2776a7a2df4ec13904c89..14eabef1e8f041710ae77fa68dce14c53364cec5 100755 (executable)
@@ -1,7 +1,5 @@
 # Testing - crib sheet
 
-Thanks to Gordon Webster, the [Digital Biologist](http://www.digitalbiologist.com), for allowing use of his [Python DNA function](http://www.digitalbiologist.com/2011/04/code-tutorial-getting-started-with-python.html).
-
 ## Detecting errors
 
 What we know about software development - code reviews work. Fagan (1976) discovered that a rigorous inspection can remove 60-90% of errors before the first test is run. M.E., Fagan (1976). [Design and Code inspections to reduce errors in program development](http://www.mfagan.com/pdfs/ibmfagan.pdf). IBM Systems Journal 15 (3): pp. 182-211.
@@ -10,82 +8,102 @@ What we know about software development - code reviews should be about 60 minute
 
 ## Runtime tests
 
-[dna.py](python/dna/dna.py)
-
-Dictionary stores molecular weights of 4 standard DNA nucleotides, A, T, C and G
+[morse04.py](python/morse/morse04.py)
 
-Function takes DNA sequence as input and returns its molecular weight, which is the sum of the weights for each nucelotide in the sequence,
+    $ python morse.py
+    encode
+    1 + 2 = 3
 
-    $ nano dna.py
-    weight = calculate_weight('GATGCTGTGGATAA')
-    print weight
-    print calculate_weight(123)
+`KeyError` is an exception.
 
-`TypeError` is an exception, raised, here, by `for...in`.
+Traceback shows us Python's exception stack trace.
 
 Runtime tests can make code robust and behave gracefully.
 
-        try:
-            for ch in sequence:
-                weight += NUCLEOTIDES[ch]
-            return weight
-        except TypeError:
-            print 'The input is not a sequence e.g. a string or list'
+    try:
+        print "Encoded is '%s'" % translator.encode(message)
+    except KeyError:
+        print 'The input should be a string of a-z, A-Z, 0-9 or space'
 
 Exception is caught by the `except` block.
 
-Pass error to another part of program e.g. UI
+Exception can be converted and passed e.g. if this was deep within a function we would not want to print but to keep UI separate so, can `raise` an exception e.g.
+
+    except KeyError:
+        raise ValueError('The input should be a string of a-z, A-Z, 0-9 or space')
 
-    except TypeError:
-        raise ValueError('The input is not a sequence e.g. a string or list')
+## Exercise
+
+Add a runtime test for decoding.
 
 ## Correctness tests
 
-Test implementation is correct.
+Testing manually works but is time-consuming and error prone - might forget to run a test.
+Write down set of test steps so won't forget. 
+Still time-consuming.
+
+    def test(self):
+        print "SOS is ", self.encode('SOS')
+        print "...---... is ", self.decode('... --- ...')
+
+Extend UI to invoke:
 
-    print "A is ", calculate_weight('A')
-    print "G is ", calculate_weight('G')
-    print "GA is ", calculate_weight('GA')
+    while True:
+
+        elif line == "test":
+            print "Testing..."
+            translator.test()
+            break
 
 Automate checking.
 
-    assert calculate_weight('A') == 131.2
-    assert calculate_weight('G') == 329.2
-    assert calculate_weight('GA') == 460.4
+    def test(self):
+        assert '... --- ...' == self.encode('SOS')
+        assert 'sos' == self.decode('... --- ...')
+        print "OK"
 
 `assert` checks whether condition is true and, if not, raises an exception.
 
-Define constants in one place only.
-
-    assert calculate_weight('A') == NUCLEOTIDES['A']
-    assert calculate_weight('G') == NUCLEOTIDES['G']
-    assert calculate_weight('GA') == NUCLEOTIDES['G'] + NUCLEOTIDES['A']
+Put test functions in separate file for modularity.
 
-Define test functions for modularity.
+    $ cp morse.py test_morse.py
+    $ nano test_morse.py
 
-    def test_a():
-        assert calculate_weight('A') == NUCLEOTIDES['A']
-    def test_g():
-        assert calculate_weight('G') == NUCLEOTIDES['G']
-    def test_ga():
-        assert calculate_weight('GA') == NUCLEOTIDES['G'] + NUCLEOTIDES['A']
+    from morse import MorseTranslator
 
-    test_a()
-    test_g()
-    test_ga()
+    class TestMorseTranslator:
 
-Put test functions in separate file for modularity.
+        def test(self):
+            translator = MorseTranslator()
+            assert '... --- ...' == translator.encode('SOS')
+            assert 'sos' == translator.decode('... --- ...')
 
-    $ nano test_dna.py
+    if __name__ == "__main__":    
 
-Import dictionary and function.
+        test_translator = TestMorseTranslator()
+        test_translator.test()
+        print "OK"
 
-    from dna import calculate_weight
-    from dna import NUCLEOTIDES
+Remove test code from `MorseTranslator`.
 
 Run tests.
 
-    $ python test_dna.py
+    $ python test_morse.py
+
+Modularise functions.
+
+    def test_encode_sos(self):
+        ...
+    def test_decode_sos(self):
+        ...
+
+    test_translator.test_encode_sos()
+    test_translator.test_decode_sos()
+
+Remove duplicated code:
+
+    def __init__(self):
+        self.translator = MorseTranslator()
 
 Test function,
 
@@ -93,11 +111,11 @@ Test function,
 * Runs function / component on inputs to get actual outputs.
 * Checks actual outputs match expected outputs. 
 
-Verbose, but equivalent, version of `test_a`.
+Verbose, but equivalent, version of `test_encode_sos`.
 
-    def test_a():
-        expected = NUCLEOTIDES['A']
-        actual = calculate_weight('A')                     
+    def test_encode_sos(self):
+        expected = '... --- ...'
+        actual = self.translator.encode('SOS')                     
         assert expected == actual
 
 ## `nose` - a Python test framework
@@ -108,67 +126,57 @@ Verbose, but equivalent, version of `test_a`.
 
 `test_` file and function prefix.
 
-    $ nosetests test_dna.py
+    $ nosetests test_morse.py
 
 `.` denotes successful tests.
 
-    # Remove `test_` function calls.
-    $ nosetests test_dna.py
+Remove `__main__ code.
+
+    $ nosetests test_morse.py
 
 xUnit test report, standard format, convert to HTML, present online.
 
     $ nosetests --with-xunit test_dna.py
     $ cat nosetests.xml
 
-    $ python
-    >>> from nose.tools import *
-    >>> expected = 123
-    >>> actual = 123
-    >>> assert_equal(expected, actual)
-    >>> actual = 456
-    >>> assert_equal(expected, actual)
-    >>> expected = "GATTACCA"
-    >>> actual = ["GATC", "GATTACCA"]
-    >>> assert_true(expected in actual)
-    >>> assert_false(expected in actual)
-    >>> assert_true("GTA" in actual, "Expected value was not in the output list")
-
 ## Propose some more tests. 
 
 Consider,
 
 * What haven't we tested for so far? 
-* Have we covered all the nucleotides? 
-* Have we covered all the types of string we can expect?
-* In addition to test functions, other types of runtime test could we add to `calculate_weight`?
+* Have we covered all possible strings?
+* Have we covered all possible arguments?
 
-Examples.
+Propose examples and add to Etherpad.
 
-    calculate_weight('T')
-    calculate_weight('C')
-    calculate_weight('TC')
-    calculate_weight(123)
+    encode('sos')
+    encode('')
+    decode('')
+    encode('1 + 2 = 3')
+    decode('...---...')
 
-Test for the latter,
+Implement examples.
 
-    try:
-        calculate_weight(123) 
-        assert False
-    except ValueError:
-        assert True
+Tests for illegal arguments:
 
-Alternatively,
+    def test_encode_illegal(self):
+        try:
+            self.translator.encode('1 + 2 = 3')
+            assert False
+        except KeyError:
+            assert True
+
+Alternatively:
 
     from nose.tools import assert_raises
 
-    def test_123():
-        assert_raises(ValueError, calculate_weight, 123)
+    def test_encode_illegal(self):
+        assert_raises(KeyError, self.translator.encode, '1 + 2 = 3')
 
-Another run-time test, for `GATCX`,
+Testing components together:
 
-        ...
-    except KeyError:
-        raise ValueError('The input is not a sequence of G,T,C,A')
+    assert 'sos' == decode(encode('sos'))
+    assert '... --- ...' == encode(decode('... --- ...'))
 
 ## Testing in practice
 
@@ -239,62 +247,6 @@ Example.
         # TODO - will complete this tomorrow!
         pass
 
-## Test-driven development
-
-Complement, cDNA, mapping,
-
-* A => T
-* C => G
-* T => A
-* G => C
-
-DNA strand GTCA, cDNA strand CAGT. 
-
-Antiparallel DNA - calculate inverse of cDNA by reversing it.
-
-[Test driven development](http://www.amazon.com/Test-Driven-Development-By-Example/dp/0321146530) (TDD)
-
- * 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.
-
-Think about what code should do and test what it should do rather than what it actually does.
-
-YAGNI principle (You Ain't Gonna Need It) avoid developing code for which there is no need.
-
-    $ nano test_dnautils.py
-    from dnautils import antiparallel
-
-    $ nosetests test_dnautils.py
-    $ nano dnautils.py
-
-    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-case string. 
-        """
-        pass
-
-    $ nosetests test_dnautils.py
-
-Propose a test and add it.
-
-    $ nosetests test_dnautils.py
-
-Change function to make test pass.
-
-Propose some tests and implement these, but don't update the function.
-
-Discuss!
-
-Update the function.
-
-TDD delivers tests and a function - no risk of us leaving the tests "till later" (never!).
-
-Refactor code with security of tests to flag any changes made introduce a bug.
-
 ## Summary
 
 Testing
diff --git a/testing/python/dna/dna.py b/testing/python/dna/dna.py
deleted file mode 100755 (executable)
index 4cf4f0e..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-
-NUCLEOTIDES = {'A':131.2, 'T':304.2, 'C':289.2, 'G':329.2}
-
-def calculate_weight(sequence):
-    """
-    Calculate the molecular weight of a DNA sequence.
-
-    @param sequence: DNA sequence expressed as an upper-case string.
-    @return molecular weight.
-    """
-    weight = 0.0
-    for ch in sequence:
-        weight += NUCLEOTIDES[ch]
-    return weight
diff --git a/testing/python/exercise/dnautils.py b/testing/python/exercise/dnautils.py
deleted file mode 100755 (executable)
index b7cbddc..0000000
+++ /dev/null
@@ -1,38 +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')
-
-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-case 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-case string. 
-    """
-    return inverse(complement(sequence))
diff --git a/testing/python/exercise/test_dnautils.py b/testing/python/exercise/test_dnautils.py
deleted file mode 100755 (executable)
index 5bccb3c..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-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)
diff --git a/testing/python/morse.py b/testing/python/morse.py
new file mode 100644 (file)
index 0000000..16a10a8
--- /dev/null
@@ -0,0 +1,82 @@
+
+import string
+import sys
+
+class MorseTranslator:
+    """This class can translate to and from morse code."""
+    def __init__(self):
+        self._letter_to_morse = {'a':'.-', 'b':'-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
+                                 'g':'--.', 'h':'....', 'i':'..', 'j':'.---', 'k':'-.-', 'l':'.-..', 'm':'--', 
+                                 'n':'-.', 'o':'---', 'p':'.--.', 'q':'--.-', 'r':'.-.', 's':'...', 't':'-',
+                                 'u':'..-', 'v':'...-', 'w':'.--', 'x':'-..-', 'y':'-.--', 'z':'--..',
+                                 '0':'-----', '1':'.----', '2':'..---', '3':'...--', '4':'....-',
+                                 '5':'.....', '6':'-....', '7':'--...', '8':'---..', '9':'----.',
+                                 ' ':'/', '':'' }
+
+        self._morse_to_letter = {}
+
+        for letter in self._letter_to_morse:
+            morse = self._letter_to_morse[letter]
+            self._morse_to_letter[morse] = letter
+
+    def encode(self, message):
+        """This function encodes the passed message into morse,
+           and returns the morse code string"""
+        morse = []
+
+        for letter in message:
+            letter = letter.lower()
+            morse.append(self._letter_to_morse[letter])
+
+        return string.join(morse," ")
+
+    def decode(self, message):
+        """This function decodes the passed morse code message
+           and returns a string containing the decoded message"""
+
+        english = []
+
+        # Now we cannot read by letter. We know that morse letters are
+        # separated by a space, so we split the morse string by spaces
+        morse_letters = string.split(message, " ")
+
+        for letter in morse_letters:
+            english.append(self._morse_to_letter[letter])
+
+        # Rejoin, but now we don't need to add any spaces
+        return string.join(english,"")
+
+if __name__ == "__main__":    
+
+    translator = MorseTranslator()
+
+    while True:
+        print "Instruction (encode, decode, quit) :-> ",
+
+        # Read a line from standard input
+        line = sys.stdin.readline()
+        line = line.rstrip()
+
+        # the first line should be either "encode", "decode"
+        # or "quit" to tell us what to do next...
+        if line == "encode":
+            # read the line to be encoded
+            message = sys.stdin.readline().rstrip()
+
+            print "Message is '%s'" % message
+            print "Encoded is '%s'" % translator.encode(message)
+
+        elif line == "decode":
+            # read the morse to be decoded
+            message = sys.stdin.readline().rstrip()
+
+            print "Morse is   '%s'" % message
+            print "Decoded is '%s'" % translator.decode(message)
+
+        elif line == "quit":
+            print "Exiting..."
+            break
+
+        else:
+            print "Cannot understand '%s'. Instruction should be 'encode', 'decode' or 'quit'." % line
+