© Sridhar Anandakrishnan 2018
Sridhar AnandakrishnanPropeller Programminghttps://doi.org/10.1007/978-1-4842-3354-2_4

4. Test-Driven Development

Sridhar Anandakrishnan
(1)
Department of Geosciences, University Park, Pennsylvania, USA
 
One of the techniques I will be using is Test-Driven Development (TDD). You don’t need to use TDD, but I find it helpful as a learning aide. TDD is rapidly becoming the norm in large software projects. The advantages of this method of development are manifold. In collaborative projects, developers can ensure that their changes don’t inadvertently break something elsewhere. In long-lived projects, when you return to something from a long time ago, you can study the tests to maintain the code. Even in small projects (such as this one), using TDD will give you confidence that the results are correct.
The idea behind TDD is that you write a specification of what the program is supposed to do; you then write a test for a small piece of the program. Then, and only then, write the minimal amount of code that lets the test pass. Then write another test and another piece of code to pass that test…and run all the tests every time to make sure your changes haven’t broken anything. Once you have enough tests that cover the specification and all the tests pass, you are done. If you ever change the code (or the specification), then you rerun the tests.
For example, here is my specification for a program, SQR, to square a number:
  1. 1.
    SQR(i) SHALL return the square of i, so long as the result doesn’t overflow a 32-bit number.
     
  2. 2.
    The program SHALL return -1 if the square of the number would overflow.
     
The first test, TEST_THAT_SQR_2_IS_4, runs SQR(2) and then asks the question “Is the result 4?” The function prints OK or FAIL depending on whether the answer is TRUE or FALSE. It will also print an informational message. (The full code is on GitHub; Listing 4-1 shows the parts related to TDD.)
 1  ' set up clock here - more on that later
 2  ...
 3
 4  PUB MAIN
 5
 6    'Initialize the Terminal and TDD here - more on that later
 7  ...
 8
 9    " run the tests
10    TEST_THAT_SQR_2_IS_4
11    TEST_THAT_SQR_BIG_IS_NEG1
12    TEST_THAT_SQR_NEG_BIG_IS_NEG1
13
14  PUB SQR(x)
15    return x*x
16
17  PUB TEST_THAT_SQR_2_IS_4 | t0
18    t0 := 4 == SQR (2)
19    TDD.ASSERT_TRUTHY(t0, string("Test that SQR (2) == 4"))
20
21  PUB TEST_THAT_SQR_BIG_IS_NEG1 | t0
22    t0 := -1 == SQR(1<<30)
23    TDD.ASSERT_TRUTHY(t0, string("Test that SQR(big) == -1"))
24
25  PUB TEST_THAT_SQR_NEG_BIG_IS_NEG1 | t0
26    t0 := -1 == SQR(-(1<<30))
27    TDD.ASSERT_TRUTHY(t0, string("Test that SQR(-big) == -1"))
Listing 4-1
Spin Program to Demonstrate TDD (ch04/tdd 0.spin)
  • Lines 11–13: Calls to the testing methods .
  • Lines 15–16: The method under test, SQR.
  • Lines 18–20: A method to test that 2×2 == 4. t0 will be true if SQR(2) returns 4. The method TDD.ASSERT_TRUTHY takes two arguments: t0 and a string. It prints out the string and then either OK or FAIL depending on whether t0 is true or false.
  • Lines 22–28: Two additional tests.
TDD is simply a formal layer over what people do in an ad hoc manner when programming. You write your code and run it with some example inputs and verify that it works. With TDD, that process is saved with the code that you are developing and can (and should) be rerun every time you make changes to the code. Ideally, you are striving for 100 percent coverage, where the tests traverse all the lines of code that you have written by appropriately setting the inputs. By convention, the tests are simple and test a small piece of the code. By running all the tests, you hope to cover all the lines of the program under test. Also, by convention, the test names are verbose and grammatical so that they are self-documenting.
Let’s run the tests. As you can see, there is a problem with the code. The tests are written to meet my specification, but the code fails some of my tests. It passes the test to square a small number but not to square a large one.
Propeller Version 1 on /dev/cu.usbserial - A103FFE0
Loading tdd_0 . binary to hub memory
4308 bytes sent
Verifying RAM ... OK
[Entering terminal mode. Type ESC or Control -C to exit.]
Test that SQR (2) == 4
... ok
Test that SQR(big) == -1
*** FAIL
The test for overflow after multiplication has failed. Let’s modify SQR so that it passes those tests. Spin has two forms of multiplication. The standard from, *, will restrict the result to 32 bits. The alternative form, **, will return the upper 32 bits of the multiplication.
1  PUB SQR(x) | t
2    t := x ** x ' multiply and return high long
3    if t
4      return -1
5    return x*x
Let’s rerun the tests and ensure that all the tests pass.
Test that SQR (2) == 4
... ok
Test that SQR(big) == -1
... ok
Test that SQR(-big) == -1
... ok

4.1 TDD Spin Code

The TDD Spin library is quite simple (see Listing 4-2). Define variables local to TDD (debugging port, number of tests run, etc.) and then print out the message and test result.
 1  { TestDrivenDevelopment .spin: Test Driven Development }
 2
 3  VAR
 4      byte debug, nTest, nPass, nFail
 5
 6  OBJ
 7    UARTS : " FullDuplexSerial4portPlus_0v3 " '1 COG for 3 serial ports
 8
 9  DAT
10    OK byte "... ok", 13, 10, 0
11    FAIL byte "*** FAIL", 13, 10, 0
12
13  PUB INIT(debugport)
14      debug := debugport
15      nTest := nPass := nFail := 0
16
17  PUB ASSERT_TRUTHY(condition, msg)
18      nTest++
19      UARTS.PUTC(debug, 13)
20      UARTS.PUTC(debug, 10)
21      UARTS.STR(debug, msg)
22      UARTS.PUTC(debug, 13)
23      UARTS.PUTC(debug, 10)
24      'UARTS.DEC(debug, t)
25      if condition <> 0
26         UARTS.STR(debug, @OK)
27         nPass++
28         return TRUE
29      else
30         UARTS.STR(debug, @FAIL)
31         nFail++
32         return FALSE
33
34  PUB SUMMARIZE
35    UARTS.STR(DEBUG, string(" Tests Run: "))
36    UARTS.DEC(DEBUG, nTest)
37    UARTS.PUTC(DEBUG, 13)
38    UARTS.PUTC(DEBUG, 10)
39    UARTS.STR(DEBUG, string(" Tests Passed : "))
40    UARTS.DEC(DEBUG, nPass)
41    UARTS.PUTC(DEBUG, 13)
42    UARTS.PUTC(DEBUG, 10)
43    UARTS.STR(DEBUG, string(" Tests Failed : "))
44    UARTS.DEC(DEBUG, nFail)
45    UARTS.PUTC(DEBUG, 13)
46    UARTS.PUTC(DEBUG, 10)
Listing 4-2
TDD Library (libs/TestDrivenDevelopment.spin)
  • Lines 3–4: Variables local to TDD that are set/reset by the INIT method.
  • Lines 9–11: A data block in which you reserve memory for the message strings. These are used over and over again, and rather than generate them each time, you can define and declare them once. The symbol OK is a series of bytes made of the ASCII characters for …ok and CR and LF (carriage return and line feed, respectively). The final 0 informs the printer that the string terminates here.
  • Lines 13–15: Initialize the variables.
  • Lines 17–32: Print out the message and either OK or FAIL. In line 26 and line 30, I pass the address of the memory space I reserved in the DAT section. UARTS.STR will print out the bytes at @OK or @FAIL up to the terminating NULL (0).
  • Lines 34–46: Print out a summary.

4.2 Summary

In Test-Driven Development, you first write the specification. Next, you write a test to exercise one part of the specification. It will fail. So, you write the code to allow the test to pass. When it passes, you write another test and the associated code, and so on. By the end, you should have tests that fully represent the specification, and you should have code that passes all the tests. If you so much as add a comment, you rerun all the tests!
If you ever change your specification, then you write new tests to cover those changes. If you ever change the code (be it ever so trivial a change!), rerun the tests. In collaborative projects, this is particularly important. When different people submit their updated code, the tests can be run automatically, and any failed tests are immediately apparent. This is called continuous integration and is aimed at reducing the manual labor of merging many different workers.
Now that we have a testing framework in place, shall we plow on? Figure 4-1 shows the work needed to clear a path forward.
A459910_1_En_4_Fig1_HTML.jpg
Figure 4-1
Train in snowdrift, Bernina Railway, Switzerland. Source: CJ Allen, The Steel Highway, 1928.