Unit tests

This chapter describes procedures that, given a candidate procedure, test whether this candidate procedure satisfies some properties.

The notion of unit test

When declaring a procedure, typically,

  1. we think about what this procedure should do, and
  2. we write how this procedure should do what it should do, i.e., we program it.

Then, there is a blissfully empty moment where we are happy that the procedure is written in a syntactically correct way. This moment, however, doesn’t last: We haven’t tested it, and the sad truth is that programs that have not been tested most often do not work. This knowledge borne of experience tells us that the tests are likely to show that our procedure doesn’t quite work, if it works at all. So we drag our feet. Besides, what are tests, and how does one write tests? Isn’t it the job of someone else to write tests? Then we could blame them for their tests, not us for our procedure.

See how the previous paragraph drifted from the message (science) to the messenger (psychology)? The issue, however, is merely one of timing. We should simply write the tests first, i.e., at the time where we are thinking about what the procedure should do.

Take a procedure that implements the addition of natural numbers, for example. What do we expect it to do? Well, if we apply it to two arguments – e.g., 2 and 3, it should return their sum – i.e., 5, here. And if we apply it to two other arguments – e.g., 12 and 43, it should return their sum – i.e., 55, here. And if we apply it to two other arguments – e.g., 123 and 432, it should return their sum – i.e., 555, here.

So let us write a procedure – a “unit-test procedure” – that carries out these three tests. Our unit-test procedure is given a candidate procedure and it checks that applying it to two arguments yields the expected result. Here is Version 0:

(define unit-tests-for-add_v0
  (lambda (candidate)
    (and (equal? (candidate 2 3)
                 5)
         (equal? (candidate 12 43)
                 55)
         (equal? (candidate 123 432)
                 555)
         ;;; add more tests here
         )))

The first thing to do is to test our unit-test procedure with a witness procedure that we know is implementing the addition function correctly. Here, the resident addition procedure fits perfectly:

> (unit-tests-for-add_v0 +)
#t
>

The naming convention for testing procedures is to name them unit-tests-for-foo if the procedure we want to test is named foo.

Exercise 01

Implement a fake procedure add_v0 that satisfies unit-tests-for-add_v0 and yet does not implement the addition function.

Solution for Exercise 01

Anton: That’s a job for us!

Bong-sun: Let me see. The fake procedure only needs to work properly for the tested values.

Anton: Right. So if it is applied to 2 and 3, it should yield 5, if it is applied to 12 and 43, it should yield 55, and if it is applied to 123 and 432, it should yield 555.

Alfrothul: And if it is applied to any other non-negative integer, it can return anything. How about 0?

Bong-sun: Why not. Let’s go:

(define fake-add_v0
  (lambda (i j)
    (if (and (= i 2) (= j 3))
        5
        (if (and (= i 12) (= j 43))
            55
            (if (and (= i 123) (= j 432))
                555
                0)))))

Pablito: Let me try, let me try:

> (unit-tests-for-add_v0 fake-add_v0)
#t
>

Halcyon: Yay! Our fake addition procedure passes the unit tests!

Alfrothul: We also need to show that it is incorrect:

> (= (fake-add_v0 1 2) (+ 1 2))
#f
>

Anton: So all in all, our fake procedure is incorrect and yet it passes the unit tests.

Mimer (soberly): What Dijkstra said.

Edsger W. Dijkstra (facetiously): What did I say?

Ray Charles: “Yeah yeah, what’d I say, all right / Well, tell me what’d I say, yeah.”

The fourth wall: We are seriously drifting here.

Halcyon: Mr Charles! Thanks for swinging by!

Mimer: Prof. Dijkstra! Thanks for passing by!

Dana: Actually, we are not drifting that much here. Prof. Dijkstra’s point is that testing only proves the presence of errors, not their absence.

Bong-sun: And here, the tests do not reveal that the fake procedure is incorrect.

Alfrothul: But the extra test does reveal that the fake procedure is incorrect.

Bong-sun: What do these tests say about the unit-test procedure?

Alfrothul: Well, we tested it with the resident addition procedure and with our fake procedure.

Dana: And these two tests do not reveal that our unit-test procedure is not complete and therefore erroneous.

Pablito: So we must test our unit-test procedures too?

Mimer: We must test our unit-test procedures too.

Interlude

Bong-sun: Hum.

Dana: Yes, Bong-sun?

Bong-sun: The definition of fake-add_v0 is quite a mouthful.

Dana: Well, three inputs are tested, so there are three tests.

Bong-sun: Right. But we could write it more concisely, look:

(define fake-add_v00
  (lambda (i j)
    (if (or (and (= i 2) (= j 3))
            (and (= i 12) (= j 43))
            (and (= i 123) (= j 432)))
        (+ i j)
        0)))

Dana: I see. You just single out the three inputs and you then defer to the resident addition.

Bong-sun: Yup. And it passes the unit tests too:

> (unit-tests-for-add_v0 fake-add_v00)
#t
>

Dana: OK, that’s both more concise and more readable.

Bong-sun: Looks like, yes.

Exercise 02

What do you make of the following unit tests for the factorial procedure?

(define unit-tests-for-fac_v0
  (lambda (candidate)
    (and (equal? (candidate 0)
                 1)
         (equal? (candidate 1)
                 1)
         (equal? (candidate 2)
                 2)
         (equal? (candidate 3)
                 6)
         (equal? (candidate 4)
                 24)
         (equal? (candidate 5)
                 121)
         ;;; add more tests here
         )))

Testing properties

To gain generality, unit-test procedures should not only test particular cases, they should also test properties of the candidate procedure.

For example, one could test that the candidate procedure is commutative:

(define unit-tests-for-add_v1
  (lambda (candidate)
    (and (equal? (candidate 2 3)
                 5)
         (equal? (candidate 12 43)
                 55)
         (equal? (candidate 123 432)
                 555)
         (equal? (candidate 2 5)
                 (candidate 5 2))
         ;;; add more tests here
         )))

Exercise 03

  1. Expand unit-tests-for-add_v1 to test that the candidate procedure is associative and that 0 is neutral on its left and on its right.

    Reminders:

    • Addition is associative because for all integers a, b, and c, the equality a + (b + c) = (a + b) + c holds.
    • 0 is neutral on the left of addition because for all integers a, the equality 0 + a = a holds.
    • 0 is neutral on the right of addition because for all integers a, the equality a + 0 = a holds.
  2. Implement a fake addition procedure fake_add_v1 that satisfies your expansion of unit-tests-for-add_v1.

Exercise 04

Consider the following unit tests for the multiplication procedure over non-negative integers:

(define unit-tests-for-mul_v0
  (lambda (candidate)
    (and (equal? (candidate 0 0)
                 0)
         (equal? (candidate 2 2)
                 4)
         ;;; add more tests here
         )))
  1. What is the result of applying unit-tests-for-add_v0 to +? Why is it so?
  2. What is the result of applying unit-tests-for-mul_v0 to *? Why is it so?
  3. What is the result of applying unit-tests-for-add_v0 to *? Why is it so?
  4. What is the result of applying unit-tests-for-mul_v0 to +? Why is it so?

Subsidiary questions:

  1. Rather than applying unit-tests-for-add_v0 to +, shouldn’t we apply it to (lambda (n1 n2) (+ n1 n2))? Why?
  2. Rather than applying unit-tests-for-mul_v0 to *, shouldn’t we apply it to (lambda (n1 n2) (* n1 n2))? Why?
  3. Rather than applying unit-tests-for-add_v0 to *, shouldn’t we apply it to (lambda (n1 n2) (+ n1 n2))? Why?
  4. Rather than applying unit-tests-for-mul_v0 to +, shouldn’t we apply it to (lambda (n1 n2) (* n1 n2))? Why?

And one more for the road

For no other reason that we will have the use of it in the next chapter, here are some “quick and dirty” unit tests for the exponentiation procedure:

(define unit-tests-for-exp_v0
  (lambda (candidate)
    (and (equal? (candidate 10 2)
                 100)
         (equal? (candidate 2 10)
                 1024)
         ;;; add more tests here
         )))

(The term “quick and dirty” means “(1) that was programmed quickly and with no particular care for elegance nor efficiency, and (2) that hopefully is not incorrect”.)

Resources

Version

Created [07 Sep 2025]