Skip to main content

Laboratory: Unit Testing with RackUnit

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.

Preliminaries

a. After starting DrRacket, add the following lines to your definitions pane and click run:

#lang racket
(require gigls/unsafe)
(require rackunit)
(require rackunit/text-ui)

b. Read the following procedures to make sure that you understand what they do, and then add the code to your definitions pane.

;;; Procedure:
;;;   irgb-scale
;;; Parameters:
;;;   color, an integer-encoded RGB color
;;;   factor, a real number
;;; Purpose:
;;;   Scale each component in irgb by factor
;;; Produces:
;;;   scaled, an integer-encoded RGB color
;;; Preconditions:
;;;   0 <= factor <= 1
;;; Postconditions:
;;;   (irgb-red scaled) = (floor (* scale (irgb-red color)))
;;;   (irgb-green scaled) = (floor (* scale (irgb-green color)))
;;;   (irgb-blue scaled) = (floor (* scale (irgb-blue color)))
(define irgb-scale
  (lambda (color factor)
    (irgb (* factor (irgb-red color))
          (* factor (irgb-green color))
          (* factor (irgb-blue color)))))

;;; Procedure:
;;;   irgb-half
;;; Parameters:
;;;   color, an integer-encoded RGB color
;;; Purpose:
;;;   Scale each component of color by half
;;; Produces:
;;;   half, an integer-encoded RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   (irgb-red half) = (floor (* 1/2 (irgb-red color)))
;;;   (irgb-green half) = (floor (* 1/2 (irgb-green color)))
;;;   (irgb-blue half) = (floor (* 1/2 (irgb-blue color)))
(define irgb-half (section irgb-scale <> 1/2))

Exercises

Exercise 1: Preconditions and Postconditions

Suppose we are going to check the accuracy of an implementation of (my-irgb-average color1 color2) that computes the average of two RGB colors.

a. Sketch the preconditions of my-irgb-average.

b. Sketch the postconditions of my-irgb-average.

Exercise 2: A Simple Experiment

Traditionally, we might “test” an implementation of this procedure by creating some different colors and showing them. For example, here is some code that students might have written.

> (define fave1 (irgb 150 0 100))
> (define fave2 (irgb 0 50 0))
> (define fave3 (irgb 100 100 100))
> (image-show (color-swatch fave1
                            (my-irgb-average fave1 fave2)
                            fave2
                            (my-irgb-average fave2 fave3)
                            fave3))

They would then “eyeball” the images to make sure that that the colors seem “right”. The second color should be between the first and middle; the fourth color should be between the middle and last.

What potential flaws do you see in their testing process?

Exercise 3: Converting to RackUnit

To avoid some of the flaws you may have noted in the prior approach, many programmers express their tests using a framework like RackUnit. Doing so allows them to automatically check results without having to spend the effort of looking at them by hand.

Here is a simple test suite based on the prior experiments. Note that we’ve hard-coded the colors so that we don’t have to look elsewhere for their values.

;;; Name:
;;;   test-my-irgb-average
;;; Type:
;;;   Test suite
;;; Value:
;;;   Tests of the my-irgb-average procedure
(define test-my-irgb-average
  (test-suite
   "Tests of my-irgb-average"
   (test-case
    "Example one"
    (check-equal? (irgb-red (my-irgb-average (irgb 150 0 100) (irgb 0 50 0)))
                  75 
                  "red")
    (check-equal? (irgb-green (my-irgb-average (irgb 150 0 100) (irgb 0 50 0)))
                  25 
                  "green")
    (check-equal? (irgb-blue (my-irgb-average (irgb 150 0 100) (irgb 0 50 0)))
                  50 
                  "blue"))
  (test-case
   "Example two"
   (check-equal? (irgb-red (my-irgb-average (irgb 0 50 0) (irgb 100 100 100)))
                 50 
                 "red")
   (check-equal? (irgb-green (my-irgb-average (irgb 0 50 0) (irgb 100 100 100)))
                 75 
                 "green")
   (check-equal? (irgb-blue (my-irgb-average (irgb 0 50 0) (irgb 100 100 100)))
                 50 
                 "blue"))))

Note that the "red", "green", or "blue" that is the last parameter to each call to check-equal? gives a word to print if that comparison fails.

a. What do you see as advantages or disadvantages of this approach as compared to one from the previous exercise?

b. List (but do not add) any additional tests that you think might be useful.

Exercise 4: Testing an Implementation

Here is an implementation of my-irgb-average, based on the rule that “the average of a and b is half the sum of a and b”.

(define my-irgb-average
  (lambda (color1 color2)
    (irgb-half (irgb-add color1 color2))))

a. Does it seem to be correct? Why or why not?

b. Do you expect it to pass the test suite we designed?

c. Run the test suite to see whether or not it passes. You can use the following command.

> (run-tests test-my-irgb-average)

d. Although this implementation passes the test suite, the implementation is not correct. Why not? Because when we add two components, we cap the sum at 255. Our tests have not checked for that case. Write an additional test case that averages (irgb 200 180 200) and (irgb 100 180 180) and checks that the components of the result are 150, 180, and 190.

e. Determine whether or not the revised test suite finds an error in my-irgb-average. (It should.)

Exercise 5: Another Implementation

Here is a revised implementation of my-irgb-average that avoids the overflow issue by halving each color before adding. It follows the rule that “the average of a and b is the sum of half a and half b”.

(define my-irgb-average
  (lambda (color1 color2)
    (irgb-add (irgb-half color1) (irgb-half color2))))

a. Does it seem to be correct? Why or why not?

b. Do you expect it to pass the revised test suite?

c. Run the test suite to see whether or not it passes.

d. There is a subtle error in this implementation. Identify that error and insert a test to catch that error.

Exercise 6: Improving Our Test Suite

The test suite, as written, is a bit clumsy. First, we have nearly identical code for each of the four or so major tests. Second, we have to remember to enter the same pair of colors three times per test. Third, we need to do the computation of the average value by hand. When we are doing repeated work, it is often helpful to have a separate procedure.

;;; Procedure:
;;;   test-case-my-irgb-average
;;; Parameters:
;;;   name, a string
;;;   color1, an integer-encoded RGB color
;;;   color2, an integer-encoded RGB color
;;; Purpose:
;;;   Generate a test case for my-irgb-average
;;; Produces:
;;;   case, a test-case
(define test-case-my-irgb-average
  (lambda (name color1 color2)
    (test-case
     name
     (check-equal? (irgb-red (my-irgb-average color1 color2))
                   (floor (* 1/2 (+ (irgb-red color1) (irgb-red color2))))
                   "red")
     (check-equal? (irgb-green (my-irgb-average color1 color2))
                   (floor (* 1/2 (+ (irgb-green color1) (irgb-green color2))))
                   "green")
     (check-equal? (irgb-blue (my-irgb-average color1 color2))
                   (floor (* 1/2 (+ (irgb-blue color1) (irgb-blue color2))))
                   "blue"))))

Now, we can rewrite that first test case much more concisely.

(define test-my-irgb-average
  (test-suite
   "Tests of my-irgb-average"
   (test-case-my-irgb-average "Example one" 
                              (irgb 150 0 100) 
                              (irgb 0 50 0))
   ...))

Isn’t that easier to read and to write?

a. Add test-case-my-irgb-average to your definitions pane.

b. Rewrite your test suite to use Add test-case-my-irgb-average.

c. Rerun your tests. You should get the same error as you were getting previously.

Exercise 7: Additional Tests

Add any other tests that you think will be useful. In writing these tests, you should think about other possible “edge” or “corner” cases.

Exercise 8: And Another Implementation

Write a version of my-irgb-average that you expect to pass the tests.

For Those With Extra Time

If you find that you have time left over, you may try any of these extra problems. (You need not do them in order.)

Extra 1: Testing irgb-scale

Write a test suite for irgb-scale.

Extra 2: Listing Cases

You will be testing a procedure that takes two colors as input. Make a list of good pairs of colors to work with. (Ideally, you’ll come up with a list of pairs of colors that will make good tests for a variety of procedures.)