Getting started with TDD: Manipulating dates

Một phần của tài liệu Manning clojure in action 2nd (Trang 271 - 279)

In this section, you’ll develop some code in a test-first manner. The first example is a set of functions that help with strings that represent dates. Specifically, you’ll write functions to increment and decrement such date strings. Such operations are often needed in many applications, so this functionality may prove useful as a utility. Although this example is simple, it illustrates the technique of writing unit tests and then getting them to pass, while also using the REPL to make the process quicker.

In TDD, you begin by writing a test. Obviously, because no code exists to sup- port the test, it will fail. Making that failing test pass becomes the immediate goal, and this process repeats. So the first thing you’ll need is a test, which you’ll start writing next.

In this simple example, the test you’ll write is for a function that can accept a string containing a date in a particular format, and you’ll check to see if you can access its components.

10.1.1 First assertion

In this initial version of the test, you’ll check that the day portion is correct. Consider the following code (remember to put it in a file called date_operations_spec.clj in a folder named clj_in_act/ch10 within your source directory):

(ns clj-in-act.ch10.date-operations-spec (:require [clojure.test :refer :all]

[clj-in-act.ch10.date-operations :refer :all])) (deftest test-simple-data-parsing

(let [d (date "2009-01-22")]

(is (= (day-from d) 22))))

You’re using the clojure.test unit-testing library for Clojure, which began life as an independent project and was later included as part of the distribution. There are other open source unit-testing libraries for Clojure (such as Midje (https://github.com/

marick/Midje), expectations (http://jayfields.com/expectations/), and others), but for most purposes, the basic clojure.test is sufficient. The first evidence that you’re looking at a unit test is the use of the deftest macro. Here’s the general form of this macro:

(deftest [name & body])

249 Getting started with TDD: Manipulating dates in strings

It looks somewhat like a function definition, without any parameters. The body here represents the code that will run when the unit test is executed. The clojure.test library provides a couple of assertion macros, the first being is, which was used in the previous example. You’ll see the use of the other macro in the following paragraphs.

Meanwhile, let’s return to the test. If you try to evaluate the test code at the REPL, Clojure will complain that it can’t find the clj-in-act.ch10.date-operations name- space. The error might look something like the following:

FileNotFoundException Could not locate clj_in_act/ch10/date_operations __init.class or clj_in_act/ch10/date_operations.clj on classpath:

clojure.lang.RT.load (RT.java:443)

To move past this error, create a new namespace in an appropriately located file.

This namespace has no code in it, so your test code still won’t evaluate, but the error will be different. It will complain that it’s unable to find the definition of a function named date:

CompilerException java.lang.RuntimeException: No such var: clj-in- act.ch10.date-operations/date, compiling:(NO_SOURCE_PATH:1:1)

Getting past this error is easy; define a date function in your new date-operations namespace. To begin with, it doesn’t even have to return anything. The same goes for the day-from function:

(ns clj-in-act.ch10.date-operations) (defn date [date-string])

(defn day-from [d])

This will cause your test to evaluate successfully, leaving it ready to be run. You can also do this from the REPL, like so:

(use 'clojure.test)

;=> nil

(run-tests 'clj-in-act.ch10.date-operations-spec) Testing clj-in-act.ch10.date-operations-spec

FAIL in (test-simple-data-parsing) (NO_SOURCE_FILE:1) expected: (= (day-from d) 22)

actual: (not (= nil 22))

Ran 1 tests containing 1 assertions.

1 failures, 0 errors.

;=> {:type :summary, :test 1, :pass 0, :fail 1, :error 0}

Now you’re set. You have a failing test that you can work on, and once you have it pass- ing, you’ll have the basics of what you want. To get this test to pass, you’ll write some real code in the clj-in-act.ch10.date-operations namespace. One way to imple- ment this functionality is to use classes from the JDK standard library (there are other options as well, such as the excellent Joda Time library available as open source).

You’ll stick with the standard library, specifically with the GregorianCalendar and the

SimpleDateFormat classes. You can use these to convert strings into dates. You can experiment with them on the REPL:

(import '(java.text SimpleDateFormat))

;=> java.text.SimpleDateFormat

(def f (SimpleDateFormat. "yyyy-MM-dd"))

;=> #'user/f

(.parse f "2010-08-15")

;=> #inst "2010-08-15T05:00:00.000-00:00"

So you know SimpleDateFormat will work, and now you can check out the Gregorian- Calendar:

(import '(java.util GregorianCalendar))

;=> java.util.GregorianCalendar (def gc (GregorianCalendar.))

;=> #'user/gc

Now that you have an instance of GregorianCalendar in hand, you can set the time by parsing a date string and then calling setTime:

(def d (.parse f "2010-08-15"))

;=> #'user/d (.setTime gc d)

;=> nil

Because setTime returns nil, you’re going to have to explicitly pass back the Gregorian- Calendar object. Once you’ve performed this experiment, you can write the code, which ends up looking like this:

(ns clj-in-act.ch10.date-operations (:import (java.text SimpleDateFormat)

(java.util Calendar GregorianCalendar))) (defn date [date-string]

(let [f (SimpleDateFormat. "yyyy-MM-dd") d (.parse f date-string)]

(doto (GregorianCalendar.) (.setTime d))))

;=> #'clj-in-act.ch10.date-operations/date (date "2010-08-15")

;=> #inst "2010-08-15T00:00:00.000-05:00"

Also, you have to figure out the implementation of day-from. A look at the API docu- mentation for GregorianCalendar reveals that the get method is what you need. You can try it at the REPL:

(import '(java.util Calendar))

;=> java.util.Calendar

(.get gc Calendar/DAY_OF_MONTH)

;=> 15

251 Getting started with TDD: Manipulating dates in strings

Again, you’re all set. The day-from function can be

(defn day-from [d]

(.get d Calendar/DAY_OF_MONTH))

The test should pass now. Remember that for the REPL to see the new definitions of the code in the date-operations namespace, you may need to reload it (using the :reload option). Here’s the output:

(run-tests 'clj-in-act.ch10.date-operations-spec) Testing clj-in-act.ch10.date-operations-spec Ran 1 tests containing 1 assertions.

0 failures, 0 errors.

;=> {:type :summary, :test 1, :pass 1, :fail 0, :error 0}

Now that you can create date objects (represented by instances of GregorianCalendar) and can access the day from these objects, you can implement accessors for month and year. Again, you’ll begin with writing a test.

10.1.2 month-from and year-from

The test for getting the month and year is similar to what you wrote before. You can include these assertions in the previous test:

(deftest test-simple-data-parsing (let [d (date "2009-01-22")]

(is (= (month-from d) 1)) (is (= (day-from d) 22)) (is (= (year-from d) 2009))))

This won’t evaluate until you at least define the month-from and year-from functions.

You’ll skip over the empty functions and write the implementation as

(defn month-from [d]

(inc (.get d Calendar/MONTH))) (defn year-from [d]

(.get d Calendar/YEAR))

With this code in place, the test should pass:

(run-tests 'clj-in-act.ch10.date-operations-spec) Testing clj-in-act.ch10.date-operations-spec Ran 1 tests containing 3 assertions.

0 failures, 0 errors.

;=> {:type :summary, :test 1, :pass 3, :fail 0, :error 0}

Again, you’re ready to add more features to your little library. You’ll add an as-string function that can convert your date objects into the string format.

10.1.3 as-string

The test for this function is quite straightforward, because it’s the same format you began with:

(deftest test-as-string (let [d (date "2009-01-22")]

(is (= (as-string d) "2009-01-22"))))

Because you have functions to get the day, month, and year from a given date object, it’s trivial to write a function that constructs a string containing words separated by dashes. Here’s the implementation, which will compile and run after you include clojure.string in the namespace via a require clause:

(require '[clojure.string :as str]) (defn as-string [date]

(let [y (year-from date) m (month-from date) d (day-from date)]

(str/join "-" [y m d])))

You can confirm that this works by running it at the REPL:

(def d (clj-in-act.ch10.date-operations/date "2010-12-25"))

;=> #'user/d (as-string d)

;=> "2010-12-25"

So that works, which means your test should pass. Running the test now gives the fol- lowing output:

(run-tests 'clj-in-act.ch10.date-operations-spec) Testing clj-in-act.ch10.date-operations-spec FAIL in (test-as-string) (NO_SOURCE_FILE:1) expected: (= (as-string d) "2009-01-22") actual: (not (= "2009-1-22" "2009-01-22")) Ran 2 tests containing 4 assertions.

1 failures, 0 errors.

;=> {:type :summary, :test 2, :pass 3, :fail 1, :error 0}

The test failed! The problem is that instead of returning "2009-01-22", your as-string function returns "2009-1-22", because the various parts of the date are returned as numbers without leading zeroes even when they consist of only a single digit. You’ll either have to change your test (which is fine, depending on the problem at hand) or pad such numbers to get your test to pass. For this example, you’ll do the latter:

(defn pad [n]

(if (< n 10) (str "0" n) (str n))) (defn as-string [date]

(let [y (year-from date) m (pad (month-from date)) d (pad (day-from date))]

(str/join "-" [y m d])))

253 Getting started with TDD: Manipulating dates in strings

Running the test now should show a better response:

(run-tests 'clj-in-act.ch10.date-operations-spec) Testing clj-in-act.ch10.date-operations-spec Ran 2 tests containing 4 assertions.

0 failures, 0 errors.

;=> {:type :summary, :test 2, :pass 4, :fail 0, :error 0}

So, you now have the ability to create date objects from strings, get at parts of the dates, and also convert the date objects into strings. You can either continue to add features or take a breather to refactor your code a little.

10.1.4 Incrementing and decrementing

Because you’re just getting started, we’ll postpone refactoring until after adding one more feature: adding functionality to advance and turn back dates. You’ll start with addition, and then you’ll write a test:

(deftest test-incrementing-date (let [d (date "2009-10-31") n-day (increment-day d)]

(is (= (as-string n-day) "2009-11-01"))))

This test will fail, citing the inability to find the definition of increment-day. You can implement this function using the add method on the GregorianCalendar class, which you can check on the REPL:

(def d (date "2009-10-31"))

;=> #'user/d

(.add d Calendar/DAY_OF_MONTH 1)

;=> nil (as-string d)

;=> "2009-11-01"

So that works quite nicely, and you can convert this into a function, as follows:

(defn increment-day [d]

(doto d

(.add Calendar/DAY_OF_MONTH 1)))

Now, you can add a couple more assertions to ensure you can add not only days but also months and years. The modified test looks like this:

(deftest test-incrementing-date (let [d (date "2009-10-31") n-day (increment-day d) n-month (increment-month d) n-year (increment-year d)]

(is (= (as-string n-day) "2009-11-01")) (is (= (as-string n-month) "2009-11-30")) (is (= (as-string n-year) "2010-10-31"))))

The code to satisfy this test is simple, now that you already have increment-day:

(defn increment-month [d]

(doto d

(.add Calendar/MONTH 1))) (defn increment-year [d]

(doto d

(.add Calendar/YEAR 1)))

Running this results in the following output:

(run-tests 'clj-in-act.ch10.date-operations-spec) Testing clj-in-act.ch10.date-operations-spec FAIL in (test-incrementing-date) (NO_SOURCE_FILE:1) expected: (= (as-string n-day) "2009-11-01") actual: (not (= "2010-12-01" "2009-11-01")) FAIL in (test-incrementing-date) (NO_SOURCE_FILE:1) expected: (= (as-string n-month) "2009-11-30") actual: (not (= "2010-12-01" "2009-11-30")) FAIL in (test-incrementing-date) (NO_SOURCE_FILE:1) expected: (= (as-string n-year) "2010-10-31") actual: (not (= "2010-12-01" "2010-10-31")) Ran 4 tests containing 8 assertions.

3 failures, 0 errors.

;=> {:type :summary, :test 4, :pass 5, :fail 3, :error 0}

All the tests failed! Even the one that was passing earlier (incrementing the date by a day) is now failing. Looking closely, all three failures are because the incremented date seems to be "2010-12-01". It appears that "2009-10-31" was incremented first by a day, then by a month, and then by a year! You’ve been bitten by the most- Java-objects-are-not-immutable problem. Because d is a mutable object, and you’re calling increment-day, increment-month, and increment-year on it, you’re accu- mulating the mutations, resulting in a final date of "2010-12-01". (As a side note, this also illustrates how easy it is to get used to Clojure’s immutability and then to expect everything to behave like Clojure’s core data structures. Within a few days of using Clojure, you’ll begin to wonder why you ever thought mutable objects were a good idea!)

To address this problem, you’ll return a new date from each mutator function. The clone method in Java does this, and you can use it in your new definitions:

(defn increment-day [d]

(doto (.clone d)

(.add Calendar/DAY_OF_MONTH 1))) (defn increment-month [d]

(doto (.clone d)

(.add Calendar/MONTH 1))) (defn increment-year [d]

(doto (.clone d)

(.add Calendar/YEAR 1)))

255 Getting started with TDD: Manipulating dates in strings

With this change, all the tests pass, allowing us to now tackle decrementing. Again, you’ll start with a test:

(deftest test-decrementing-date (let [d (date "2009-11-01") n-day (decrement-day d) n-month (decrement-month d) n-year (decrement-year d)]

(is (= (as-string n-day) "2009-10-31")) (is (= (as-string n-month) "2009-10-01")) (is (= (as-string n-year) "2008-11-01"))))

To get this test to pass, you can go with the same structure of functions that did the incrementing. The code might look like the following:

(defn decrement-day [d]

(doto (.clone d)

(.add Calendar/DAY_OF_MONTH -1))) (defn decrement-month [d]

(doto (.clone d)

(.add Calendar/MONTH -1))) (defn decrement-year [d]

(doto (.clone d)

(.add Calendar/YEAR -1)))

Each function calls an appropriate Java method, and this passes all the tests. You now have code that works and a library that can accept date strings and return dates as strings. It can also increment and decrement dates by days, months, and years. But the code isn’t quite optimal, so you’re now going to improve it.

10.1.5 Refactor mercilessly

Extreme programming (XP) is an Agile methodology that espouses several specific guidelines. One of them is to “refactor mercilessly.” It means that you should continu- ously strive to make code (and design) simpler by removing clutter and needless com- plexity. An important part of achieving such simplicity is to remove duplication. You’ll do that with the code you’ve written so far.

Before you start, it’s pertinent to make an observation. There’s one major require- ment to any sort of refactoring: for it to be safe, there needs to be a set of tests that can verify that nothing broke because of the refactoring. This is another benefit of writing tests (and TDD in general). The tests from the previous section will serve this purpose.

You’ll begin refactoring by addressing the duplication in the increment/decrement functions. Here’s a rewrite of those functions:

(defn date-operator [operation field]

(fn [d]

(doto (.clone d)

(.add field (operation 1)))))

(def increment-day (date-operator + Calendar/DAY_OF_MONTH)) (def increment-month (date-operator + Calendar/MONTH))

(def increment-year (date-operator + Calendar/YEAR))

(def decrement-day (date-operator - Calendar/DAY_OF_MONTH)) (def decrement-month (date-operator - Calendar/MONTH)) (def decrement-year (date-operator - Calendar/YEAR))

After replacing all six of the old functions with this code, the tests still pass. You’ve removed the duplication from the previous implementation and also made the code more declarative: the job of each of the six functions is clearer with this style. The ben- efit may seem small in this example, but for more complex code, it can be a major boost in readability, understandability, and maintainability. This refactored version can be further reduced via some clever use of convention, but it may be overkill for this particular task. As it stands, you’ve reduced the number of lines from 18 to 10, showing that the old implementation was a good 80% larger than this new one.

Imagine a similar refactoring being applied to the month-from, day-from, and year-from functions. What might that look like?

This section showed how to use the built-in Clojure unit-testing library called clojure.test. As you saw through the course of building the example, using the REPL is a critical element to writing Clojure code. You can use the REPL to quickly check how things work and then write code once you understand the APIs. It’s great for such short experiments and allows for incrementally building up code for larger, more complex functions. When a unit-testing library is used alongside the REPL, the combination can result in an ultrafast development cycle while keeping quality high.

In the next section, you’ll see how you can write a simple mocking and stubbing library to make your unit testing even more effective.

Một phần của tài liệu Manning clojure in action 2nd (Trang 271 - 279)

Tải bản đầy đủ (PDF)

(338 trang)