### 技术控

今日:162| 主题:52609

# [其他] Interactive Development with Clojure.spec

257 11

### 立即注册CoLaBug.com会员，免费获得投稿人的专业资料，享用更多功能，玩转个人品牌！

x
clojure.spec provides seamless integration with      clojure.test.check's generators. Write a spec, get a functioning generator, and you can use that generator in a REPL as you're developing, or in a generative test.
To explore this, we'll use clojure.spec to specify a scoring function for Codebreaker, a game based on an old game called Bulls and Cows, a predecessor to the board game, Mastermind. You might recognize this exercise if you've read      The RSpec Book, however this will be a bit different.
Although I'll explain some things as I go along, I'm going to assume that you're already familiar with the      clojure.spec Rationale and Overviewand the      spec Guide.
If you like, you can follow along by evaluating the forms (in order of appearance - some redef vars that are def'd earlier in the namespace) in      codebreaker.clj.
Problem

We want a function that accepts a secret code and a guess, and returns a score for that guess. Codes are made of 4 to 6 colored pegs, selected from six colors: [r]ed, [y]ellow, [g]reen, [c]yan, lack, and [w]hite. The score is based on the number of pegs in the guess that match the secret code. A peg in the guess that matches the color of the peg in the same position in the secret code is considered an exact match, and a peg that matches a peg in a different position in the secret code is considered a loose match.
For example, if the secret code is      [:r :y :g :c]and the guess is      [:c :y :g :b], the score would be      {:codebreaker/exact-matches 2 :codebreaker/loose-matches 1}because      :yand      :gappear in the same positions and      :cappears in a different position.
We want to invoke this fn with two codes and get back a map like the one above, e.g.

1. (score [:r :y :g :c] [:c :y :g :r])
2. ;; {:codebreaker/exact-matches 2
3. ;;  :codebreaker/loose-matches 2}

Properties and property-based testing

In property-based testing, we make assertions about properties of a function and provide test data generators. The testing tool then generates test data, applies the function to it, and invokes the assertions. Properties are more general than examples in example based tests. For example, rather than writing a test that expresses the example above and asserts that the resulting map looks exactly like the one above, we'd write expressions that express more general properties like:

• The return value should be a map with the two keys        :codebreaker/exact-matchesand        :codebreaker/loose-matches
• the values should be natural (i.e. non-negative) integers
• the sum of the values should be        >=0
• the sum of the values should be        <=data-preserve-html-node="true" the number of pegs in the secret code
We'll express all of these properties using clojure.spec, and we're also going to describe the arguments to the function using the same tooling. This is one way in clojure.spec departs from other property-based testing tools.
First, in English:

• there are two arguments
• the arguments should both be        codes

• a code is a sequence of 4 to 6 colored pegs
• the available colors are red, yellow, green, cyan, black, and white, represented by            :r,            :y,            :g,            :c,            :b, and            :w
• a code may contain duplicates

• the two codes should be of equal length
:args

We have a few more questions to answer, but that's enough to get started, which we'll do with a function spec. We'll start with just the spec for the arguments, and a couple of supporting definitions.

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))

Now we can exercise the      :argsspec:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])

s/exercisereturns a collection of tuples of a value generated by the generator associated with the spec, and the same value conformed by the spec. This can give us a lot of confidence that the spec expresses what we think it does and that the generator produces the values we expect. The generator we get for free from the      :argsspec is sufficient, so we don't need to explicitly define one.
The output reveals that we didn't yet specify one of the properties described earlier: the two codes should be of equal length. So let's specify that:

1. (s/fdef score
2.         :args (s/and (s/cat :secret ::code :guess ::code)
3.                      (fn [{:keys [secret guess]}]
4.                        (= (count secret) (count guess)))))
5. (s/exercise (:args (s/get-spec `score)))
6. ;; ([([:w :w :y :b :c] [:c :b :y :w :c]) {:secret [:w :w :y :b :c], :guess [:c :b :y :w :c]}]
7. ;;  [([:y :g :b :r :b] [:b :y :r :b :r]) {:secret [:y :g :b :r :b], :guess [:b :y :r :b :r]}]
8. ;;  ...
9. ;;  [([:y :w :g :w :g] [:y :c :c :y :y]) {:secret [:y :w :g :w :g], :guess [:y :c :c :y :y]}]
10. ;;  [([:b :w :c :r :c :w] [:b :g :r :y :y :g]) {:secret [:b :w :c :r :c :w], :guess [:b :g :r :y :y :g]}])

:ret

Now the      :argsspec represents the properties we laid out above, so let's move on to spec the      :retspec.

1. (s/def ::exact-matches nat-int?)
2. (s/def ::loose-matches nat-int?)
3. (s/fdef score
4.         :args (s/and (s/cat :secret ::code :guess ::code)
5.                      (fn [{:keys [secret guess]}]
6.                        (= (count secret) (count guess))))
7.         :ret (s/keys :req [::exact-matches ::loose-matches]))
8. (s/exercise (:ret (s/get-spec `score)))
9. ;; ([{:codebreaker/exact 0, :codebreaker/loose 1} {:codebreaker/exact 0, :codebreaker/loose 1}]
10. ;;  [{:codebreaker/exact 0, :codebreaker/loose 0} {:codebreaker/exact 0, :codebreaker/loose 0}]
11. ;;  ...
12. ;;  [{:codebreaker/exact 0, :codebreaker/loose 10} {:codebreaker/exact 0, :codebreaker/loose 10}]
13. ;;  [{:codebreaker/exact 0, :codebreaker/loose 15} {:codebreaker/exact 0, :codebreaker/loose 15}])

Again, we see tuples of a generated value and the same value conformed by the spec. And here we see that the map keys are correct but the map values may exceed the number of pegs in the code, violating one of the properties we laid out earlier: the sum of the values in the returned map should be between 0 and the count of either of the codes. The values generated by the      :retspec are always      >= 0because they are spec'd with      nat-int?, and their sum is therefore always      >= 0, but we can't specify that the sum is      <=data-preserve-html-node="true" the number of pegs without knowing the number of pegs, and that information is in the      :argsspec, which is not exposed to the      :retspec.
:fn

For relationships between      :argsand      :retvalues, we use a      :fnspec:

1. (s/fdef score
2.         :args (s/and (s/cat :secret ::code :guess ::code)
3.                      (fn [{:keys [secret guess]}]
4.                        (= (count secret) (count guess))))
5.         :ret (s/keys :req [::exact-matches ::loose-matches])
6.         :fn (fn [{{secret :secret} :args ret :ret}]
7.               (<= 0 (apply + (vals ret)) (count secret))))

Here we're choosing to explicitly specify that the sum of the values is      <= 0even though it's already specified implicitly by the      nat-int?predicates we used to specify the values in the returned map. This is not necessary, but it clearly does a better job of expressing the properties we described earlier.
So now we have specs for the      :args, the      :retvalue, and the relationship between them (in the      :fnspec). So we're done, right? Well, not quite. We still don't have a function!
Wire it up

We need a function to tie it all together. Here's a skeletal implementation:

1. (defn score [secret guess]
2.   {::exact-matches 0
3.    ::loose-matches 0})
4. (s/exercise-fn `score)
5. ;; ([([:g :w :w :c :y :g] [:b :c :g :w :c :w]) {:codebreaker/exact-matches 0, :codebreaker/loose-matches 0}]
6. ;;  [([:y :r :w :c :b] [:b :b :c :r :c]) {:codebreaker/exact-matches 0, :codebreaker/loose-matches 0}]
7. ;;  ...
8. ;;  [([:r :w :r :y :r :r] [:g :w :g :y :r :r]) {:codebreaker/exact-matches 0, :codebreaker/loose-matches 0}]
9. ;;  [([:y :c :r :c] [:y :r :g :c]) {:codebreaker/exact-matches 0, :codebreaker/loose-matches 0}])

This is incomplete, but we can see that the generated args and the function's return value match the specs, including the      :fnspec. So now let's use clojure.spec's test/check wrapper to actually test the function:

1. (stest/check `score)
2. ;; ...
3. ;; :clojure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1471029622166},

This happens to pass because the 0 values conform to the      ::exact-matchesand      ::loose-matchesspecs, and their sum conforms to the      :retspec. We can validate that the test is actually testing what we think it is by providing hard coded values that would not conform to the spec, e.g.

1. (defn score [secret guess]
2.   {::exact-matches 4
3.    ::loose-matches 3})
4. (s/exercise-fn `score)
5. ;; ([([:y :w :b :g :g] [:c :c :r :r :y]) {:codebreaker/exact-matches 4, :codebreaker/loose-matches 3}]
6. ;;  [([:y :y :b :g :c] [:r :b :w :y :g]) {:codebreaker/exact-matches 4, :codebreaker/loose-matches 3}]
7. ;;  ...
8. ;;  [([:r :w :y :g] [:y :r :y :g]) {:codebreaker/exact-matches 4, :codebreaker/loose-matches 3}])
9. ;;     (stest/check `score)
10. ;;       :clojure.spec.test.check/ret {:result #error {
11. ;;  :cause "Specification-based check failed"
12. ;;  :data {:clojure.spec/problems
13. ;;         [{:path [:fn]
14. ;;           :pred (fn [{{secret :secret} :args, ret :ret}]
15. ;;                   (<= 0 (apply + (vals ret)) (count secret)))
16. ;;           :val {:args {:secret [:w :b :w :w :r :g]
17. ;;                        :guess  [:c :c :b :c :r :c]}
18. ;;           :ret #:codebreaker{:exact-matches 4, :loose-matches 3}}
19. ;;           :via []
20. ;;           :in []}]
21. ;;  :clojure.spec.test/args ([:w :b :w :w :r :g] [:c :c :b :c :r :c])
22. ;;  :clojure.spec.test/val {:args {:secret [:w :b :w :w :r :g]
23. ;;                                 :guess  [:c :c :b :c :r :c]}
24. ;;                          :ret #:codebreaker{:exact-matches 4, :loose-matches 3}}
25. ;;  :clojure.spec/failure :check-failed}

Now we know that everything's wired up correctly, and we can start to flesh out the solution.
The approach we'll take is to calculate all of the matches, ignoring position, and then the exact matches, and then subtract the exact matches from all matches to calculate the number of loose matches. For example, given the secret      [:r :g :y :c]and the guess      [:g :w :b :c], there are 2 matches altogether,      :gand      :c, so the count of all matches would be 2. One of those,      :c, is an exact match, so exact matches would be 1, leaving 1 loose match.
exact-matches

The exact match calculation is easy to imagine: we need to compare each peg in the guess to the peg in the same position in the secret:

1. (defn score [secret guess]
2.   {::exact-matches (count (filter true? (map = secret guess)))
3.    ::loose-matches 0})

Let's exercise that and see what we get:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))0

And here we can see the incredible value we get from generators! In this one sample set we got at least one case each of 0, 1, 2, and 3 exact matches. This is not guaranteed, of course. We got lucky! And the next time we run it we'll get different combinations that may or may not be as lucky. But this is far easier than imagining different scenarios, and the result is an arguably more effective evaluation of the function we've written.
You can easily scan these examples visually and validate that they're all producing the correct result for      :codebreaker/exact-matches. We can run      stest/checkagain and see that we're still passing:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))1

One thing that we don't have, however, is a specification for the exact-matches function. In fact, we don't even have an exact matches function, so let's extract it:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))2

Now we can add a spec for it. It has the same args as      score, so let's extract the      :argsspec to something we can share:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))3

And now we can use that      ::secret-and-guessspec for our      exact-matchesspec:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))4

This is quite similar to the      scorespec, but the      :retis a single      nat-int?between 0 and the count of pegs in the secret. So let's exercise this:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))5

Again, we can scan the results to validate them visually, and then run      stest/checkto validate the results against the      :fnspec.

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))6

And now we can start tying things together by instrumenting      exact-matchesand exercising and testing      score.      s/instrumentwraps a fn in a fn that checks args for conformance to the      :argsspec before delegating to the original fn:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))7

Everything still passes because      scoreis invoking      exact-matchescorrectly, but it would report incorrect calls to      exact-matchesif we had a bug in      score:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))8

This is a really great way to uncover problems below the surface. It is similar to a mock object in that incorrect calls to      exact-matchesthrow errors, but different in that nothing happens when there are no calls to      exact-matches. Still, we're in an interactive session here, and we can see that      scoreis calling      exact-matches. Gray box testing FTW!
Also note that we have property based tests for both functions, with no specific examples codified. More on this later!
If you're following along in a REPL, don't forget to fix the bug by restoring the      scorefn:

1. (ns codebreaker
2.   (:require [clojure.spec :as s]
3.             [clojure.spec.test :as stest]))
4. (def peg? #{:y :g :r :c :w :b})
5. (s/def ::code (s/coll-of peg? :min-count 4 :max-count 6))
6. (s/fdef score
7.         :args (s/cat :secret ::code :guess ::code))9

all-matches

Thinking of a function to calculate all of the matches, it would have the same properties as the      exact-matchesfunction: it takes a pair of codes and returns a      nat-int?between 0 and the count of pegs in either of the codes. We already have a spec for that, so let's generalize its name, and then we can use it for      exact-matchesand      all-matches:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])0

These calls to      s/exercise-fnand      stest/check-fnproduce similar results to those above. Now we can instrument      exact-matcheswith the      match-countspec and exercise and test      scoreas we did earlier as well:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])1

So now comes the hard part: the      all-matchescalculation. We need to allow for duplicates, so we can count the number of appearances of e.g.      :rin the secret and the guess and take the lower of the two numbers. Then we can do the same for all the colors and add up the resulting counts. For example, with a secret      [:r :r :y :b]and a guess      [:r :g :c :y], we can see that      :rappears twice in the secret and once in the guess, so the score for      :rwould be 1. Yellow appears once in each, so its score is 1. Neither      :gnor      :cever appear in the secret, so we don't count those and the total is 2. Make sense? Here's one way to express that:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])2

All together now

Scanning the output of      s/exercise, we can see we got it right. So now let's put that to work in      score:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])3

We can see that we're calculating the exact and loose matches correctly, and the tests all pass. So now we can memorialize these generative tests in a repeatable automated test.
Testing, testing, 1, 2, 3 ...

We can get a summary of the test results:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])4

If there were failures the result would look like this instead:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])5

Then we can build predicates around that like:

1. (s/exercise (:args (s/get-spec `score)))
2. ;; ([([:y :w :g :y :c] [:c :g :y :y :y :c]) {:secret [:y :w :g :y :c], :guess [:c :g :y :y :y :c]}]
3. ;;  [([:c :w :g :r :g] [:r :b :c :r :w :g]) {:secret [:c :w :g :r :g], :guess [:r :b :c :r :w :g]}]
4. ;;  ...
5. ;;  [([:r :c :w :w :y :r] [:y :r :y :y :c]) {:secret [:r :c :w :w :y :r], :guess [:y :r :y :y :c]}]
6. ;;  [([:c :g :b :g :w :b] [:r :y :w :r :b]) {:secret [:c :g :b :g :w :b], :guess [:r :y :w :r :b]}])6

And then we can hook those into assertions in clojure.test or whatever tool you prefer for repeatable tests.
Note that there are no example based tests here: everything is being generated by generators built for us by clojure.spec. If this makes you uncomfortable, then add a couple of example based tests! But you certainly don't need an exhaustive set of them. The handful of functions and specs are all small, expressive, and easy for any experienced clojure developer to read and understand, and the generative, property-based tests that we get for      score,      exact-matches, and      all-matchesprovide a lot of confidence that they are all working correctly.
Experience report

I've been using TDD in some form or another for many years, and when clojure.spec appeared I was curious to see how it would fit into or change my approach. I won't go as far as to say that this post represents "my approach" as that is still evolving in light of the presence of spec, but there are clear similarities to and differences from TDD in this example.
Like TDD, there is a tight feedback loop: write a spec and exercise it right away, all before any implementation code. The spec itself is not a test, but it      isa reusable source for generated sample data that we can use in an interactive REPL session and in a repeatable test.
Like TDD, I did some refactoring as I discovered opportunities to improve the code. Sometimes I used visual inspection of the result of exercising specs and sometimes I used test.check to cast a wider net.
Unlike TDD, I didn't go through a consistent cycle of watching a test fail and then making it pass, and then refactoring (red/green/refactor). You can use these tools for that cycle, but generative tests are, by design, more coarse than example based tests, so it might be more of a challenge to keep that very granular cycle consistent. Perhaps a subject for another post (perhaps written by you!).
Unlike example based tests in TDD, generation allowed me to quickly spot-check dozens of correct invocations without having to hand write them or wire them to assertions.
Unlike TDD, generation can sometimes find categories of inputs that we've failed to consider. That didn't happen in this example, but if you've ever forgotten to account for nil or empty string in tests for a string processing function, then you know what I'm talking about.
In summary, I think that clojure.spec provides a powerful set of tools for interactive development that should appeal to anybody developing at the REPL, regardless of your particular process.

 世界末日我都挺过去了，看到溫柔﹌散了場我才知道为什么上帝留我到现在！

 技术控老油条路过

 看来楼下的有话说!

 牛人 佩服！

a86982972 发表于 2016-10-20 23:40:29
 楼主说的，句句都是真理啊！

zxyxy 发表于 2016-10-29 03:16:00
 是爷们的娘们的都帮顶！大力支持

xiaoxi2011 发表于 2016-10-29 10:23:50
 最近回了很多帖子，都没人理我！

 已经习惯给自己第一朵了

 最近练了葵花宝典吧？

• ## Microsoft Teams in Office 365

I hope everyone would agree to the fact [...]

• ## SEO排名密码 很多人都犯错了

笔者是学软件开发出身的，毕业之后经过三四年的 [...]

• ## 全球奢侈时尚业销售收入排行 第一会是谁？

谁是全球销售额最高的时尚产业集团？随着全球各 [...]

• ## 智能电视涨价潮来袭？性价比TV推荐

从2016年下半年开始，智能电视面板就进入了紧张 [...]

• ## 再度思考马云提出的“新零售”，该如何布局

阿里巴巴集团将于2月20日公布其研究多时的“ [...]

• ## iOS 抓取 HTML ,CSS XPath 解析数据

以前我们获取数据的方式都是使用 AFN 来 Get JSO [...]

• ## 卡夫亨氏放弃收购联合利华，大公司的心思忒

当地时间2月19日， 卡夫亨氏 与联合利华共 [...]

• ## Snap ad exec Sriram Krishnan leaves, but

Flying from SF to LA every week sucks, and [...]

• ## 店铺流量暴跌？你可能犯了这4个错误！

关于流量问题，是所有卖家最关心的，尤其是搜索 [...]

© 2001-2017 Comsenz Inc. Design: Dean. DiscuzFans.