Documenting and Testing Your Game

You have a working game at this point, but you also need to consider the poor developer who’s going to pick this up six months from now (particularly if it’s you). You worked hard to pick a good data structure and implement your functions, but you need to communicate those data structures for future use.

If you write some specs, you can describe the key data structures, annotate the functions, and even generate some automated tests that check whether everything works (especially when you start making changes at some future date).

When you started working on the implementation earlier, we quickly honed in on the progress data structure and the trio of internal functions (new-progress, update-progress, and complete?) that dealt with creating, updating, and checking that data structure. Because that data structure is critical to the internals of the game, it’s also a good place to start writing specs.

The signature for new-progress takes a word and returns the initial progress value. Our spec defines the structure of the arguments and return value of that function.

 (s/fdef hangman.core/new-progress
  :args (s/cat :word ::word)
  :ret ::progress)

You haven’t defined a ::word or ::progress spec yet, but that’s ok—these show us what you need to do next.

Words are made up of letters, and you’ve already made some definitions in our code for letters that you can use. This is a common case when writing specs—often the implementation and the specs use the same predicates and talk in the same “language”, which is why specs feel so expressive.

 (s/def ::letter (set letters))

A ::letter spec is the set of valid letters, which you already defined in the random player. We also could have used the predicate valid-letter?; however, we want to have our specs act as generators as well. The valid-letter? predicate doesn’t have an automatically created generator, whereas these are provided for sets.

Now you can create a spec for a word, which is just a string that consists of at least one valid letter:

 (s/def ::word
  (s/and string?
  #(pos? (count %))
  #(every? valid-letter? (seq %)))

Clojure spec will attempt to create an automatic generator from this spec, but it uses a wide range of characters, certainly more than our narrow ::letter spec will allow. The automatic generator will produce strings of this broader character set, then filter to just strings of the allowed characters, which requires much more work than seems necessary (and it may not work at all). Instead, you should supply your own generator, tailored to our character set.

The s/with-gen macro wraps a spec and attaches a custom generator. In this case, we want to generate a collection of one or more valid letters, then create a string from those letters. In clojure.spec.gen.alpha, the fmap function takes a source generator and applies an arbitrary function to each sample, defining a new generator:

 (s/def ::word
  (s/with-gen
  (s/and string?
  #(pos? (count %))
  #(every? valid-letter? (seq %)))
  #(gen/fmap
  (​fn​ [letters] (apply str letters))
  (s/gen (s/coll-of ::letter :min-count 1)))))

You can test it out directly at the REPL:

 (gen/sample (s/gen ::word))
 -> (​"hilridqg"
 "ipllomgodmzhh"
 "xbsllzg"
 "etdjwdtvquuswpox"
 "adrgrhntbuzewbdvfa"
  ... )

Those look appropriately word-like for our purposes. You now have a spec for words, so let’s think about the ::progress spec. We decided that progress would be represented by a sequence of either letters or \_ to indicate an unguessed character. Since we added an additional character, we’ll need a new spec ::progress-letter that expands ::letter to include \_. The ::progress spec is then a collection of at least one of that expanded letter set:

 (s/def ::progress-letter
  (conj (set letters) ​\_​))
 
 (s/def ::progress
  (s/coll-of ::progress-letter :min-count 1))

That’s enough specs to start testing new-progress. You can use stest/check for that and summarize what happened with summarize-results:

 (stest/summarize-results (stest/check ​'hangman.core/new-progress​))
 {:sym hangman.core/new-progress}
 -> {:total 1, :check-passed 1}

You tested one function and it passed. However, you aren’t really testing as much as you could in this function. If you look again at the args (the word) and the return value (the progress value), there’s another constraint—both values should be the same length. You can encode this in the :fn spec of the new-progress spec, which takes a map of the conformed :args and :ret spec values. Constraints that include both the args and the ret value are always recorded in the :fn spec.

 (​defn-​ letters-left
  [progress]
  (->> progress (keep #{​\_​}) count))
 
 (s/fdef hangman.core/new-progress
  :args (s/cat :word ::word)
  :ret ::progress
  :fn (​fn​ [{:keys [args ret]}]
  (= (count (:word args)) (count ret) (letters-left ret))))

First, create a helper function letters-left to compute the number of unguessed letters in a progress data structure. You can then check that the number of letters in the input word, the number of letters in the progress, and the number of unguessed letters are all the same. Rerunning the test reveals no unseen problems, but it’s good to have the extra constraint for future changes.

Next, you can handle the update-progress function using specs you’ve already defined for ::progress, ::word, and ::letter. Again, you can add a useful :fn constraint to verify that the number of letters left unguessed is less than or equal to the number unguessed at the beginning.

 (s/fdef hangman.core/update-progress
  :args (s/cat :progress ::progress :word ::word :guess ::letter)
  :ret ::progress
  :fn (​fn​ [{:keys [args ret]}]
  (>= (-> args :progress letters-left)
  (-> ret letters-left))))

And finally, the spec for complete? is straightforward:

 (s/fdef hangman.core/complete?
  :args (s/cat :progress ::progress :word ::word)
  :ret boolean?)

Now that you’ve described the progress functions, you can turn to the main game function itself. The game function takes a word, a player, an optional verbose tag and returns a score.

We’ve not yet considered how to spec a player, but you can just check the protocol in a predicate. In the ::player spec, we want the generator to work and produce a player, so let’s just have it choose a random one of the players you’ve defined:

 (​defn​ player? [p]
  (satisfies? Player p))
 
 (s/def ::player
  (s/with-gen player?
  #(s/gen #{random-player
  shuffled-player
  alpha-player
  freq-player})))

While you could spec the verbose flag and score in-line with the game spec, pulling these out as independent specs creates better, more concrete names, which are potentially reusable. For the verbose flag, you want your tests to be quiet, so the generator is hard-coded to always return false.

 (s/def ::verbose (s/with-gen boolean? #(s/gen false?)))
 (s/def ::score pos-int?)
 
 (s/fdef hangman.core/game
  :args (s/cat :word ::word
  :player ::player
  :opts (s/keys* :opt-un [::verbose]))
  :ret ::score)

If you test this out by running check, you’ll see that everything looks good. check runs 1000 games with a random player and word. It’s also useful to verify all of the function specs you have while running these tests. You can do that by instrumenting all of the specs before running check:

 (stest/instrument (stest/enumerate-namespace ​'hangman.core​))
 => [hangman.core/update-progress hangman.core/new-progress
  hangman.core/game hangman.core/complete?]

Any time you run stest/instrument, it’s good to verify that the return value states all of the instrumented functions you expect to see as a test of reasonability.

If you rerun the check, you still see no issues, but now all of the arg specs are being verified as well, giving you greater confidence in the correctness of the code. You can then do a final check that runs check on all spec’ed functions we defined:

 (-> ​'hangman.core
  stest/enumerate-namespace
  stest/check
  stest/summarize-results)
 {:sym hangman.core/update-progress}
 {:sym hangman.core/new-progress}
 {:sym hangman.core/game}
 {:sym hangman.core/complete?}
 -> {:total 4, :check-passed 4}

You could go further with testing some of the details of the individual players, but this should give you a taste for testing with spec.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.31.125