Use Case #1: Adding an Item to the Shopping List

Let's proceed with a brief functional analysis:

  • inputs: the item
  • outputs: none

nominal process:

  1. Validate the item to be added;
  2. If the item is valid, add it to the shopping list;
  3. Otherwise, do nothing.

Throughout the application development, we will refer to a small task list (yes, another list) to remind us of what needs to be done, to stay focused, and to indicate when we are done.

When we start working on an item from the list, we will make it bold, like this. When we finish an item, we will strike it through, like this. If we think of an item not in the list, we will add it to the list.

The first test of your first application

What might this list look like for our first use case?

add an item to the shopping list
– an invalid (empty) item is not added to the shopping list

You won't wonder about the type of data or the procedure you need.

You'll wonder about the first test you need.

Writing a test is like telling a story. The story of your operation, seen from the outside.

Even if this story doesn't always turn out to be true, I prefer to start with the best API I can think of at the moment, and backtrack later if it blocks me.

Below is an example:

(I deliberately truncated the module declaration, module import, and start and end of test suite lines of code. If this is problematic for understanding, I'm ready to add them back; feel free to let me know if necessary).

;; add-grocery-test.scm
(test-assert "add-an-item"
  (let* ([test-database               #f]
         [test-database-insert        (lambda (grocery)
                                        (set! test-database (reverse (cons grocery test-database))))]
         [add-grocery                 (make-add-grocery-interactor test-database-insert)]
         [given-an-empty-grocery-list (lambda () (set! test-database '()))]
         [when-add-grocery            (lambda (name) (add-grocery name))]
         [then-grocery-is-added       (lambda (grocery) (member grocery test-database))])
    (begin
      (given-an-empty-grocery-list)
      (when-add-grocery "tomatoes")
      (then-grocery-is-added "tomatoes"))))

Explanations:

  • given-an-empty-grocery-list, when-add-grocery, and then-grocery-is-added are utility functions for a test à la Gherkin.
  • add-grocery is an interactor. It's, in a way, the central procedure of our use case.
  • test-database and test-database-insert are the test's shopping list database and the procedure for inserting an item into this database.

The test doesn't compile yet. The compiler raises the following errors:

  • make-add-grocery-interactor is not defined.

What's the minimum we can do to make it compile? Define, of course!

To make the test compile, these procedures don't need to do anything. The goal is to make the test compile, not to make it pass.

In testing jargon, these implementations are called stubs. make-add-grocery-interactor should be a procedure that takes a procedure as input and returns another procedure; this latter procedure takes a name as input and doesn't seem to have a return value.

;; add-grocery.scm
(define (make-add-grocery-interactor insert)
  (lambda (grocery)
    *unspecified*))

Now we can run the test and see it fail! Only at this point can we imagine the smallest necessary change to make the test pass.

;; add-grocery.scm
(define (make-add-grocery-interactor insert)
  (lambda (grocery)
    (insert grocery)))

add an item to the shopping list
– an invalid (empty) item is not added to the shopping list

Next!

add an item to the shopping list
an invalid (empty) item is not added to the shopping list

Write the test.

;; add-grocery-test.scm
(test-assert "do-not-add-invalid-grocery"
  (let* ([test-database             #f]
         [test-database-insert      (lambda (grocery)
                                      (set! test-database (reverse (cons grocery test-database))))]
         [add-grocery               (make-add-grocery-interactor test-database-insert)]
         [given-a-grocery-list      (lambda () (set! test-database '("mushrooms" "rice" "potatoes")))]
         [when-add-grocery          (lambda (name) (add-grocery name))]
         [then-grocery-is-not-added (lambda (grocery) (not (member grocery test-database)))])
    (begin
      (given-a-grocery-list)
      (when-add-grocery "")
      (then-grocery-is-not-added ""))))

Make the test compile and see it fail.

The test compiles but doesn't pass since the "" element has been added to the list.

Make the test pass.

;; add-grocery.scm
(define (make-add-grocery-interactor insert)
  (lambda (grocery)
    (unless (string-null? grocery)
      (insert grocery))))

Remove duplications.

These are present in the tests. Let's factor out some lines of code.

First, concerning the test's database.

;; add-grocery-test.scm
(define test-database #f)

(define (test-database-insert grocery)
  (set! test-database (reverse (cons grocery test-database))))

Next, the utility procedures.

;; add-grocery.scm
(define (given-an-empty-grocery-list)
  (set! test-database '()))

(define (given-a-grocery-list)
  (set! test-database '("mushrooms" "rice" "potatoes")))

(define (when-add-grocery name)
  ((make-add-grocery-interactor test-database-insert) name))

(define (then-grocery-is-added name)
  (member name test-database))

(define (then-grocery-is-not-added name)
  (not (then-grocery-is-added name)))

Finally, the tests.

;; add-grocery.scm
(test-assert "add-a-grocery"
  (begin
    (given-an-empty-grocery-list)
    (when-add-grocery "tomatoes")
    (then-grocery-is-added "tomatoes")))

(test-assert "do-not-add-invalid-grocery"
  (begin
    (given-a-grocery-list)
    (when-add-grocery "")
    (then-grocery-is-not-added "")))

add an item to the shopping list
an invalid (empty) item is not added to the shopping list

Wrapping up

You can copy all the following code into an add-grocery-test.scm file:

(define-module (add-grocery)
  #:export (make-add-grocery-interactor))

(define (make-add-grocery-interactor insert)
  (lambda (grocery)
    (unless (string-null? grocery)
      (insert grocery))))


(define-module (add-grocery-test)
  #:use-module (add-grocery)
  #:use-module (srfi srfi-64))

(define test-database #f)

(define (test-database-insert grocery)
  (set! test-database (reverse (cons grocery test-database))))

(define (given-an-empty-grocery-list)
  (set! test-database '()))

(define (given-a-grocery-list)
  (set! test-database '("mushrooms" "rice" "potatoes")))

(define (when-add-grocery name)
  ((make-add-grocery-interactor test-database-insert) name))

(define (then-grocery-is-added name)
  (member name test-database))

(define (then-grocery-is-not-added name)
  (not (then-grocery-is-added name)))

(test-begin "add-grocery")

(test-assert "add-a-grocery"
  (begin
    (given-an-empty-grocery-list)
    (when-add-grocery "tomatoes")
    (then-grocery-is-added "tomatoes")))

(test-assert "do-not-add-invalid-grocery"
  (begin
    (given-a-grocery-list)
    (when-add-grocery "")
    (then-grocery-is-not-added "")))

(test-end "add-grocery")

Then you can run the following command and observe a similar result:

$ guile --no-auto-compile add-grocery-test.scm
%%%% Starting test add-grocery  (Writing full log to "add-grocery.log")
# of expected passes      2