Lab: Unit testing with RackUnit

Assigned
Tuesday, 17 November 2020
Summary
In the laboratory, you will explore the ways in which small tests can help you develop and update code. You will also familiarize yourself with the RackUnit unit testing library. You will also have the opportunity to think more broadly about testing.

In this lab, you will stay with your group of four and work collaboratively to explore testing and the rackunit library. Nominate a notetaker for the group; they will be responsible for gathering up the code you write into a file called testing.rkt and turning that file in to Gradescope on behalf of the group. Make sure that testing.rkt includes require declarations for the appropriate libraries:

(require csc151)
(require rackunit)
(require rackunit/text-ui)

Throughout this lab, we will provide function examples that may use language features we have not yet introduced in this course. That is fine! The purpose of this lab is to test code rather than write new code, so focus on the intended behavior of the function rather than its implementation. (Although we will briefly explore that it is sometimes helpful to know the implementation!)

Exercise 1: Roundtable

One person should serve as driver for this exercise. The remaining people in the group are navigators.

The driver should open up DrRacket, require both the csc151, rackunit, and the rackunit/text-ui packages in their file, and copy the following function:

;;; (range1 n) -> listof integer?
;;;   n : integer?
;;; Returns the list of numbers from 1 to n, inclusive.  If
;;; n is non-positive, then returns the empty list.
(define range1
  (lambda (n)
    (map (lambda (n) (+ n 1)) (range n))))

Define a rackunit test suite for this function, range1-test-suite. To do this, use the test-suite function, e.g.,

(define range1-test-suite
  (test-suite "range1 tests"
              <test>
              <test>
              ...))

The tests should be defined using the check functions described in the reading. In your buffer, you can include an explicit call to run-tests to ensure that your tests are executed on every reload of the file:

; At the top-level...
(run-tests range1-test-suite)

This is a good practice as you are developing your program so you can quickly know if your code meets the current set of tests. However, please make sure to comment out this run-tests line when submitting your final programs so that your tests do not run automatically when our autograders check your file for syntax errors!

To develop the tests, the navigators should go around and each volunteer a test case that the driver then transcribes into the test suite. Continue going around the circle of navigators identifying test cases until your group is satisfied with the suite. Each navigator should volunteer at least one test case. In your group, you should agree on when you all feel that you have reasonably validated the function’s behavior.

When you are done, the driver should send the completed function and its test suite to the notetaker. The notetaker should then include this code in testing.rkt.

Exercise 2: Positive and negative cases

One way to organize our tests is by exploring positive and negative test cases. A positive test case is an example that exercises when the function reports “yes”—e.g., returns true, computes a result—when the inputs are “good”. A negative test case is an example that exercises when the function reports “no”—e.g., returns false, returns an error value, does not modify the input—when the inputs are “bad”.

Like before, nominate a new driver; the remainder of the group will serve as navigators. The driver should share their screen and copy the following function to their buffer.

;;; (palindrome? str) -> boolean?
;;;   str : string?
;;; Returns true iff the string s is a palindrome, i.e., str is
;;; equal to its reversal.
(define palindrome?
  (lambda (str)
    (and (string? str)
         (string=? str (list->string (reverse (string->list str)))))))

As in the previous exercise, collaboratively develop a test suite, palindrome?-test-suite, for this function. The navigators should go around and volunteer one test case at a time until you are satisfied with your test coverage. For this exercise, keep in mind the idea of positive and negative test cases.

When you are done, the driver should send the completed function and its test suite to the notetaker. The notetaker should then include this code in testing.rkt.

Exercise 3: Types and corners

Another way to organize our tests is by exploring the range of possible inputs. If the type of the input admits a finite set of values, we ought to test all those values directly. However, if an infinite set of values is possible, we need to be more judicious in what values we examine.

One way to do this is to identify corner and non-corner case values. Think of a corner case as an example input that exercises the “boundaries” of how the function ought to work. For example, if you are operating over a certain range of numbers, a corner case might be an input at the lower or upper end of that range. In contrast, the values in the middle of the range are non-corner case values. We expect that the function will likely operate in the same way over these non-corner values, so we would then surmise that we don’t have to test all of these non-corner values; a few of them will suffice!

Nominate a new driver; the remainder of the group will serve as navigators. The driver should share their screen and copy the following function to their buffer.

Note: dedup-adjacent, below, relies on aspects of Racket you do not yet know. That’s okay. You should focus on the docs and the testing that might be appropriate given those docs.

;;; (dedup-adjacent l) -> listof any?
;;;   l : listof any?
;;; Returns the original list l but with all duplicates found
;;; adjacent to each other removed from the list.  For example:
;;;   > (dedup-adjacent (list 3 4 1 5 1 1 0 9 9 9 6 5 5 1 4))
;;;   '(3 4 1 5 1 0 9 6 5 1 4)
(define dedup-adjacent
  (lambda (l)
    (cond 
      [(null? l) 
       null]
      [(null? (cdr l)) 
       l]
      [else
       (let ([c1 (car l)]
             [c2 (cadr l)]
             [rest (cddr l)])
         (if (equal? c1 c2)
             (dedup-adjacent (cons c2 rest))
             (cons c1 (dedup-adjacent (cons c2 rest)))))])))

a. As in the the previous exercise, collaboratively develop a test suite, palindrome?-test-suite, for this function. The navigators should go around and volunteer one test case at a time until you are satisfied with your test coverage. For this exercise, keep in mind the idea of types and corners.

b. Here’s a not-quite-correct version of dedup-adjacent. Do your tests identify the error? If not, you need more tests.

(define dedup-adjacent
  (lambda (l)
    (cond
      [(null? l)
       null]
      [(null? (cdr l))
       l]
      [else
       (let ([c1 (car l)]
             [c2 (cadr l)]
             [rest (cddr l)])
         (if (equal? c1 c2)
             (dedup-adjacent (cons c2 rest))
             (cons c1 (cons c2 (dedup-adjacent rest)))))])))

c. When you are done, the driver should send the completed function and its test suite to the notetaker. The notetaker should then include this code in testing.rkt.

Exercise 4: Test-driven development

You do not need to turn in this part of the lab.

Tests don’t have to be created after you write your function! Because we frequently implement a function with examples in mind to begin with, it is useful to codify these examples as tests first and then use those tests to guide development. Such a development methodology is called test-driven development where the tests drive the design of the code.

Consider the following procedure description.

;;; (describe-triangle side1 side2 side3) -> string?
;;;   side1 : rational?
;;;   side2 : rational?
;;;   side3 : rational?
;;; Describe the triangle whose three sides are as given.
;;; * If all three sides are equal, the description is "equilateral".
;;; * If exactly two sides are equal, the description is "isosceles".
;;; * If no two sides are equal, description is "scalene".
;;; * If the three sides do not describe a triangle, the description
;;;   is "non-triangle".

Here’s an incorrect implementation.

(define describe-triangle
  (lambda (side1 side2 side3)
    #f))

An incorrect implementation is enough to get us started writing tests.

a. As before, write a test suite for this function.

b. Write your own version of describe-triangle. Make sure it passes your tests.

c. Post your version in the thread set up for posting versions. (If you can’t figure out the tread, ask @staff for help.) Include your group number so that others can contact you.

d. Test at least two of the other versions of describe-triangle. (If there are no other versions, temporarily skip to the next step and then come back here later.) If you find an error, let the authors know.

e. If you hear about an error in your own version, delete it from the thread, rename the old version (e.g., to describe-triangle-bad-01), fix the error, and then repost. You might also want to add some appropriate tests.

f. Once you’ve tested some of your peers’ versions, it’s time to test some good but incorrect ones that we’ve seen in the past.

These are all versions that someone wrote, but that may have flaws. You’ll follow a similar procedure for each.

When you are ready to try one of the versions post a message in the chat for your group saying “I am reading for the first example” and one of the class staff will provide you with a sample incorrect version of describe-triangle. Run your tests. If your tests fail to identify an error, add tests until you’ve found the error. (We’d prefer that you not read the code to determine the error, but you may find it necessary to do so.) If your tests identify an error, you’re ready for another incorrect version. Post an issue with the version you received in the chat for your group. Now you’re ready for another incorrect version. (If you haven’t tested at least two of your peers’ versions, now is the time to check for those again.)

Do the same until we give you one that we say that we think is correct. (Good job if you find errors in ours!)

Congratulations! You’ve achieved the level of testing that SamR expects.

Exercise 5: Code coverage

You do not need to turn in this part of the lab.

Keep the same driver for this exercise.

As you might guess, there are multiple ways of thinking about unit testing. For test-driven design and other methodologies, we do what is normally called “black-box testing”. You can’t see into black boxes, so you can only test their behavior.

But you can often peer inside the code you are testing, particularly when you write it yourself. And if you can look at the code, you can consider one other issue. You should ask yourself: “Once I’ve run all of my tests, am I sure that every part of my code has run at least once?” It’s amazing how often there’s a little piece of code tucked inside a larger program that’s never been checked, never been tested, and, when it gets used, fails.

Look back at the final example above, which purports to be correct. Are you sure that your tests cover every facet? If not, add tests that do.

There is nothing to submit for this exercise. Just make sure that your test suite covers all the code.

When you are done, the driver should send the completed function and its test suite to the notetaker. The notetaker should then include this code in testing.rkt.