From c416b384774718977fdcb51e671b27090e7d646b Mon Sep 17 00:00:00 2001 From: Mike Jackson Date: Wed, 3 Jul 2013 13:00:54 +0100 Subject: [PATCH] Replaced DNA with morse example! --- testing/README.md | 242 +++++++++-------------- testing/python/dna/dna.py | 14 -- testing/python/exercise/dnautils.py | 38 ---- testing/python/exercise/test_dnautils.py | 26 --- testing/python/morse.py | 82 ++++++++ 5 files changed, 179 insertions(+), 223 deletions(-) delete mode 100755 testing/python/dna/dna.py delete mode 100755 testing/python/exercise/dnautils.py delete mode 100755 testing/python/exercise/test_dnautils.py create mode 100644 testing/python/morse.py diff --git a/testing/README.md b/testing/README.md index d577306..14eabef 100755 --- a/testing/README.md +++ b/testing/README.md @@ -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 index 4cf4f0e..0000000 --- a/testing/python/dna/dna.py +++ /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 index b7cbddc..0000000 --- a/testing/python/exercise/dnautils.py +++ /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 index 5bccb3c..0000000 --- a/testing/python/exercise/test_dnautils.py +++ /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 index 0000000..16a10a8 --- /dev/null +++ b/testing/python/morse.py @@ -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 + -- 2.26.2