Tests

Create a new Leiningen project:

lein new app music

In the module music.core (music/src/music/core.clj), provide a function to parse duration strings to seconds:

(defn parse-duration [dur]
  (let [matcher (re-matcher #"^([0-9]+)m([0-9]+)s$" dur)
        results (re-find matcher)
        minutes (Integer/parseInt (nth results 1))
        seconds (Integer/parseInt (nth results 2))]
    (+ (* minutes 60) seconds)))

Write a test in the module music.core-test (music/test/music/core_test.clj) using the built-in clojure.test library:

(ns music.core-test
  (:require [clojure.test :refer :all])
  (:require [music.core :as music]))

(deftest test-parse-duration
  (is (= (music/parse-duration "3m15s") 195)))

Run the test from the REPL:

(require '[music.core-test :as tests])
(tests/test-parse-duration) ; nil

The result nil stands for success (absence of errors).

Write a test using multiple is assertions:

(deftest test-parse-duration
  (is (music/parse-duration "3m15s") 195)
  (is (music/parse-duration "6m12s") 312)
  (is (music/parse-duration "2m51s") 171))

Group tests together using testing with a description:

(deftest test-parse-duration
  (testing "minutes and seconds"
    (is (= (music/parse-duration "3m15s") 195))
    (is (= (music/parse-duration "6m12s") 312))
    (is (= (music/parse-duration "2m51s") 171)))
  (testing "only minutes or seconds"
    (is (= (music/parse-duration "3m") 180))
    (is (= (music/parse-duration "17s") 17))))

The test fails because parse-duration cannot handle the input from the second group properly.

Run all the tests of a particular namespace from the REPL:

(ns music.core)
(require '[clojure.test :as test])
(test/run-tests 'music.core-test)
;; {:test 1, :pass 3, :fail 0, :error 2, :type :summary}

Run all the tests of the current namespace from the REPL:

(ns music.core-test)
(require '[clojure.test :as test])
(test/run-tests)
;; {:test 1, :pass 3, :fail 0, :error 2, :type :summary}

Run all the tests using Leiningen from the shell:

lein test

Write a parametrized test using are:

(deftest test-parse-durations
  (are [input expected] (= (music/parse-duration input) expected)
    "1s" 1
    "1m" 60
    "1m1s" 61
    "2m3s" 123
    "15m32s" 932))

Extend :dependencies in project.clj for test data generators:

:dependencies [[org.clojure/clojure "1.11.1"] ; existing
               [org.clojure/test.check "1.1.1"]] ; new

Install the additional dependency:

lein deps

Create a random test data generator for positive integers:

(require '[clojure.test.check.generators :as gen])
(def numbers-gen gen/pos-int)

Constrain the positive integers to non-zero values:

(def non-zero-numbers-gen (gen/such-that (complement zero?) gen/pos-int))

Generate maps of test data:

(def duration-gen (gen/hash-map :m non-zero-numbers-gen :s non-zero-numbers-gen))

Get a function that picks a random element from a generator:

(gen/elements duration-gen)

Write a property-based test that exercises a property on 100 generated inputs:

(ns music.core-test
  (:require [clojure.test :refer :all])
  (:require [clojure.test.check :as tc])
  (:require [clojure.test.check.clojure-test :as ctest])
  (:require [clojure.test.check.generators :as gen])
  (:require [clojure.test.check.properties :as prop])
  (:require [music.core :as music]))

(def non-zero-numbers-gen (gen/such-that (complement zero?) gen/pos-int))
(def duration-gen (gen/hash-map :m non-zero-numbers-gen :s non-zero-numbers-gen))

(ctest/defspec parse-duration-not-zero 100
  (prop/for-all [duration duration-gen]
                (let [dur (str (:m duration) "m" (:s duration) "s")]
                  (> (music/parse-duration dur) 0))))

Exercises

Flexible Duration Parsing

Make all the tests pass by improving the implementation of the given parse-duration function.

Hint: Define additional matching groups and use the regular expression quantor ? for optional groups.

Test: The provided tests shall pass, e.g. (parse-duration "3m") shall return 180, and (parse-duration "17s") shall return 17.

Solution
(defn parse-duration [dur]
  (let [matcher (re-matcher #"^(([0-9]+)(m))?(([0-9]+)(s))?$" dur)
        results (re-find matcher)
        minutes (if (nil? (nth results 3)) 0 (Integer/parseInt (nth results 2)))
        seconds (if (nil? (nth results 6)) 0 (Integer/parseInt (nth results 5)))]
    (+ (* minutes 60) seconds)))

Hours Duration Parsing

Define additional (yet failing) test cases for duration containing hour indications, e.g. "1h15m50s". Then extend the implementation of parse-duration in order to make the tests pass.

Hint: Use testing to group the tests, and h as an hour indication in the input string. Repeated multiplication with the factor 60 can be expressed using reduce.

Test: (parse-duration "2h5m3s") shall return 7503, (parse-duration "1h10m") shall return 4200, and (parse-duration "1h") shall return 3600. All defined tests shall pass.

Solution

Test:

(ns music.core-test
  (:require [clojure.test :refer :all])
  (:require [music.core :as music]))

(deftest test-parse-duration
  (testing "hours, minutes, and seconds"
    (is (= (music/parse-duration "2h5m3s") 7503)))
  (testing "hours and minutes"
    (is (= (music/parse-duration "1h10m") 4200)))
  (testing "only hours"
    (is (= (music/parse-duration "1h") 3600))))

Implementation:

(defn parse-duration [dur]
  (let [matcher (re-matcher #"^(([0-9]+)(h))?(([0-9]+)(m))?(([0-9]+)(s))?$" dur)
        results (re-find matcher)
        hours (if (nil? (nth results 3)) 0 (Integer/parseInt (nth results 2)))
        minutes (if (nil? (nth results 6)) 0 (Integer/parseInt (nth results 5)))
        seconds (if (nil? (nth results 9)) 0 (Integer/parseInt (nth results 8)))
        values [hours minutes seconds]]
    (reduce (fn [acc v] (+ (* acc 60) v)) values)))

Property-Based Hypotenuse Test

Given a function that calculates the hypotenuse given the triangle’s short sides:

(defn hypot [a b]
  (Math/sqrt (+ (Math/pow a 2) (Math/pow b 2))))

(hypot 3 4) ; 5.0

Write a property-based hypot-longest-side test to ensure that the hypotenuse is longer than both a and b, but shorter than their sum. Run it for a million samples.

Hint: Use the pos-int generator from above to generate the test data.

Test: All tests shall pass using lein run.

Solution
(ns music.core-test
  (:require [clojure.test :refer :all])
  (:require [clojure.test.check :as tc])
  (:require [clojure.test.check.clojure-test :as ctest])
  (:require [clojure.test.check.generators :as gen])
  (:require [clojure.test.check.properties :as prop]))

(def pos-int (gen/such-that (complement zero?) gen/pos-int))

(ctest/defspec hypot-longest-side 1e6
  (prop/for-all [a pos-int
                 b pos-int]
                (let [c (hypot a b)]
                  (and (> c a) (> c b) (< c (+ a b))))))