Hello, World

In keeping with tradition, I'm going to do a little demonstration of what you can expect in the rest of this book, with the famous "hello, world" exercise.

I already invite you to create a small workspace (including a folder) in which you can store the source codes of this book. Let's say: ~/Workspace/guile-handbook/.

So, for the following exercise, we'll go into the ~/Workspace/guile-handbook/hello/ folder. There, create a file hello.scm with the following contents:

(define-module (hello))

(define-public hi
  (lambda ()
    "hello world\n"))

How it works

Guile is a very permissive language. That is to say that a great freedom is left to the hacker in the way he writes his programs. When I started coding in Guile, I would have appreciated a little more structure (or a little guide…).

In my example, I chose to create a hello module via the define-module procedure. With Guile, I don't have to start with a module or a main function like in other languages. A module can be seen as a collection of procedures, variables, and macros that you choose to group together.

The define-public procedure exposes the hi symbol, which is linked to a procedure that takes no input arguments, and returns the string hello world\n.

How to test

Still in the folder ~/Workspace/guile-handbook/hello/, create a file hello-test.scm :

(use-modules (srfi srfi-64)
             (hello))

(test-begin "harness")

(test-equal "test-hello"
  "hello world\n"
  (hi))

(test-end "harness")

To run the test, execute the following command :

$ guile -L . hello-test.scm

The -L option indicates that for this command, I want the current directory (.) to be added at the beginning of the path of the Guile modules to be loaded. Otherwise, Guile wouldn't know that the hello module exists.

I prefer this way, which doesn't permanently alter the path with learning materials.

Writing tests

No need to search, choose and install a test framework. Guile comes with a module containing the `srfi-64' (a test framework for the Scheme language).

No need to prefix or suffix filename or function name.

The use-modules procedure allows us to import the modules needed for our tests:

  • the srfi-64 module
  • the hello module, which we want to test.

For the purposes of the testing framework, the test-begin and test-end procedures indicate where the test suite begins and ends, respectively. It is necessary to name it by providing a string, here: "harness".

A test is a call to one of the procedures provided by the srfi-64 module. Here, it is the test-equal procedure which takes 3 arguments:

  1. the name of the test: "test-hello"
  2. The expected value: "hello world\n"
  3. the expression we are testing: (hi).

If the expected value is equal to the value resulting from the evaluation of the tested expression, then the test passes. Otherwise, the test fails.

When running the tests, the result provided is very summary. By default, the framework gives more details in a log file name-of-the-test-suite-te-tests.log..

Hello, You

In the previous example, I wrote the code to be tested before writing the test. In the rest of this book, I will follow the steps of the Test Driven Development method: red - green - refactor.

With a test in place that can be run quickly, I can iterate on the program safely. If I break a feature, I will notice it immediately! It is called the feedback loop.

Now let's go back to the previous code and add a requirement : I want to be able to specify to whom I send my greetings.

Red

I start by adding, in my test suite, the one that reflects this requirement :

(use-modules (srfi srfi-64)
             (hello))

(test-begin "harness")

(test-equal "test-hello"
  "hello world\n"
  (hi))
  
(test-equal "test-named-hello"
  "hello Jérémy\n"
  (hi "Jérémy"))

(test-end "harness")

If you run the test suite, you can observe that our first test passes but the new test fails. This step ensures that I don't write a test that is a false positive because it doesn't test the right behavior.

;;; note: source file /home/jeko/Workspace/guile-handbook/hello/hello-test.scm
;;;       newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello-test.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /home/jeko/Workspace/guile-handbook/hello/hello-test.scm
;;; hello-test.scm:12:2: warning: possibly wrong number of arguments to `hi'
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello-test.scm.go
%%%% Starting test harness  (Writing full log to "harness.log")
hello-test.scm:10: FAIL test-named-hello
# of expected passes      1
# of unexpected failures  1

Moreover, the compiler is a great helper in explaining why a code doesn't work. Here, it warns me that in line 12 of my test file, I use the hi procedure with the wrong number of arguments.

Green

Now I can change my code to make the test to pass.

First, I start by correcting the compilation warning. In order to do that, I make the hi procedure to handle an optional parameter :

(define-module (hello))

(define-public hi
  (lambda* (#:optional name)
    "hello world\n"))

Now that the compiler isn't complaining anymore, I look in the log file of my test suite for the reason the failure, cat harness.log :

%%%% Starting test harness
Group begin: harness
Test begin:
  test-name: "test-hello"
  source-file: "hello-test.scm"
  source-line: 6
  source-form: (test-equal "test-hello" "hello world\n" (hi))
Test end:
  result-kind: pass
  actual-value: "hello world\n"
  expected-value: "hello world\n"
Test begin:
  test-name: "test-named-hello"
  source-file: "hello-test.scm"
  source-line: 10
  source-form: (test-equal "test-named-hello" "hello Jérémy\n" (hi "Jérémy"))
Test end:
  result-kind: fail
  actual-value: "hello world\n"
  expected-value: "hello Jérémy\n"
Group end: harness
# of expected passes      1
# of unexpected failures  1

The test "test-named-hello" failed because it expected the value "hello Jeremy" but got "hello world". I correct it :

(define-module (hello))

(define-public hi
  (lambda* (#:optional name)
    (if name
        "hello Jérémy\n"
        "hello world\n")))

This time, the execution of the test suite shows that the new code fixed the second test without breaking the first one.

;;; note: source file ./hello.scm
;;;       newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling ./hello.scm
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello.scm.go
%%%% Starting test harness  (Writing full log to "harness.log")
# of expected passes      2

My test suite can be viewed as a list of use cases of my software. More pragmatic than a documentation. Without any knowledge of the operational code, I can quicly see the software can be called with zero or one parameter and what it returns.

Version control

As it stands, the production code is operational, as shown by the green tests. It is recommended that you commit this state of the code before the next step (this will allow you to return to the operational version).

However, since the red-green-refactor cycle is not completed, there is no reason to push the commit. The code is functional but not completed.

Refactor

Refactor the code means eliminating duplications and making the code more explicit. For me, these are the most important principles, from which all others are derived.

And precisely, in the code, we can see duplications, both in the values and in the construction of the following strings :

"hello Jérémy \n"
"hello world \n"

That I will eliminate by extracting a procedure that will not be exposed publicly as well as constants that will bring more meaning to the values.

(define-module (hello))

(define GREETING_PREFIX "hello ")
(define GREETING_SUFFIX "\n")
(define DEFAULT_ADDRESSEE "world")

(define-public hi
  (lambda* (#:optional name)
    (string-append GREETING_PREFIX (addressee name) GREETING_SUFFIX)))

(define addressee
  (lambda (name)
    (if name
    "Jérémy"
    DEFAULT_ADDRESSEE)))

Come back on the version control

At this point, I can gladly amend the previous commit to keep only this flawless version of our code and testing.

Hello, You… again

In the previous example, I deliberately pushed the concept of "small steps" very far for pedagogical reasons. But this allows me to introduce a TDD technique: Triangulation.

Red

(use-modules (srfi srfi-64)
             (hello))

(test-begin "harness")

(test-equal "test-hello"
  "hello world\n"
  (hi))

(test-equal "test-named-hello"
  "hello Jérémy\n"
  (hi "Jérémy"))

(test-equal "test-named-hello-bis"
  "hello Hacker\n"
  (hi "Hacker"))
  
(test-end "harness")

I can see the failed test in the test suite.

Green

(define-module (hello))

(define GREETING_PREFIX "hello ")
(define GREETING_SUFFIX "\n")
(define DEFAULT_ADDRESSEE "world")

(define-public hi
  (lambda* (#:optional name)
    (string-append GREETING_PREFIX (addressee name) GREETING_SUFFIX)))

(define addressee
  (lambda (name)
    (if name
        name
        DEFAULT_ADDRESSEE)))

The change is minimal and obvious. This is one of the advantages of forcing oneself to work in "small steps".

Now all the tests pass.

Refactor

In the refactoring stage, you have to rework all the code. This includes your tests !

Tests are a clear specification of what the code needs to do. The more explicit the test suite is, the sooner a user will understand how the program works.

(use-modules (srfi srfi-64)
             (hello))

(test-begin "harness")

(define (assert-correct-message test-name test-expected test-effective)
  (test-equal test-name test-expected test-effective))

(assert-correct-message "test-default-hello" "hello world\n" (hi))
      
(test-equal "test-named-hello"
  "hello Jérémy\n"
  (hi "Jérémy"))

(test-equal "test-named-hello-bis"
  "hello Hacker\n"
  (hi "Hacker"))
  
(test-end "harness")

Wrapping up

  • Introduction to TDD
  • Discipline
  • Respect of Red-Green-Refactor cycle
  • Optional parameters for procedures