Lab: Unit testing with RackUnit

Assigned
Wednesday, 21 April 2021
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 definitions pane, 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 definitions pane.

;;; (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 definitions pane.

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

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 find 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 believe that you have a sufficient set of tests that will approve all correct implementations and reject all incorrect implementations, reach out to the course staff, who may provide you with an additional version of describe-triangle to check. While you are wayiting for a response, spend some time working on the current mini-project.

g. Share your tests and describe-triangle with the note-taker, who should incorporate them in the file.