How to be agile with spec?

clojure_spec

#1

Having just watched the classic Halloway introduction of Spec, “Agility & Robustness”, I thought on my last use of spec. I used it for my interface between my cljs and clj. I created specs in a cljc namespace, and used preconditions on the server side with manual assertions on the front-end. It worked, but it was FAR from agile (meaning just fast-to-put-together, not meaning scrummy). Surely there’s an idiomatic/elegant solution to doing gradual typing without writing tests/assertions/preconditions in addition to the specs themselves. What am I missing?


#2

You haven’t really given enough information about what you did vs what Stuart is advocating for me to be able to provide a cogent answer but the “tests/assertions/preconditions” aspect sounds like you were doing a lot more work than he is talking about.

Agility with Spec comes from only needing to write specs for what’s important and only needing to provide as much detail as you care about at each stage. If your server-side API is going to do validation, writing a spec for the input buys you a lot: you can run in dev with instrumentation on and trap any calls that are invalid, while you are writing code; wherever you want actual production validation you already have the spec for the data so you can just conform it, and proceed if it isn’t invalid; wherever you want validation failures reported, you can build on that code by asking spec to explain the failure; when you want to test the behavior of a function that you’ve already spec’d (for inputs), you can enhance the spec to describe the return value and/or the relationship between the inputs and the output, and then have spec test it generatively – providing far more tests than any manually written example tests are going to give you (and it will shrink any failures down to a small repro case); wherever you have a spec of data around your interface, spec can generate random, conforming data for you – that’s very valuable when you’re mocking one side or the other, or when you just want to explore functionality in the REPL without having to manually concoct example/test data.

No assertions or preconditions needed.

In summary, Spec can automatically check you are calling functions correctly, automatically check the behavior of your functions, automatically perform validation of data, automatically provide the first cut of failure reporting, and automatically generate data for exploration, mocking, and testing. That’s a lot of work you don’t have to do yourself – and you can do all of this gradually, to whatever level of detail (specificity) that you need at any point in the development cycle.


#3

Thanks for the reply. I think my problem must be something very basic I don’t understand about how to use specs. Here’s a sample of the way I wrote it; I know something must be wrong/unnecessary about how I’m using spec.

     ;;;;;;;;;;;;;;;;;;;;;
    ;; cljc
    (ns myapp.specs.assigner
      (:require [clojure.spec.alpha :as s]
		[myapp.specs :as hgs]))

    (s/def ::reviewer-type (s/keys :req-un [:myapp.specs/name :myapp.specs/email]))

    (s/def ::reviewer-1 ::reviewer-type)
    (s/def ::reviewer-2 ::reviewer-type)
    (s/def ::reviewers (s/keys :req-un [::reviewer-1 ::reviewer-2]))
    (s/def ::approval-id integer?)

    (s/def ::assign (s/keys :req-un [::reviewers ::approval-id]))

    ;;;;;;;;;;;;;;;;;;;;;
    ;; backend
    (ns myapp.routes.admin
	(:require [clojure.spec.alpha :as s]
		  [myapp.specs.assigner :as aspec]))
    (defn assign-reviewers
      "Assign the given reviewers to the application, and notify them."
      [{:keys [approval-id]
	{:keys [reviewer-1 reviewer-2]} :reviewers
	:as assignment}]
      {:pre [(s/valid? ::aspec/assign assignment)]}
      ;; logic
      )

    ;;;;;;;;;;;;;;;;;;;;;
    ;; cljs
    (ns myapp.review.assigner
      "Extras for assigner reviews"
      (:require [clojure.spec.alpha :as s]
		[myapp.specs.assigner :as aspec]))

    (defn _submit-assignment
      "Gather the current user info and approval number and submit approval"
      [submission]
      ;; details
      (if (s/valid? ::aspec/assign submission)
	  (do-stuff)
	(s/explain ::aspec/assign submission)))


#4

@seancorfield mentioned this as well, it looks like you‘re not utilizing s/fdef for function instrumentation. It unlocks a whole lot of power, I recommend reading up on instrumentation with spec, as well as maybe look at some usage examples in existing repositories (although I have no good examples on hand)


#5

It looks to me like you’re trying it as a type system, especially with the use of :pre.

One of the benefits of Spec (over a type system) is that you can evolve it over time, sketching out just the important parts as and when you need them. You might ultimately end up where your code is (roughly) but you don’t need to start there – and you only need to reach that point if a full specification is important (it’s often important to not over-specify your systems).

As I noted in my earlier reply: Spec is great for situations where you actually need data validation and you were going to have to write explicit code to validate input data and handle erroneous aspects of the data. In the case of your assign-reviewers function, when faced with invalid input, it will just blow up and produce a fair unhelpful AssertionError message (which doesn’t seem right, to me, for a server-side API function).

You’ve also presented just “source code” without considering tests – another area where Spec helps you move much faster because it can do so much to help support you during testing.

Even with the code you present above tho’, Spec has already allowed you to avoid writing quite a bit of code: you haven’t had to write a function to validate your assignment data and you haven’t had to write a function to explain invalid data (although, if you’re intended to explain failures to end users you’ll probably want to write something on top of explain-data to pretty things up for your domain eventually). You also haven’t had to write any mock data for testing (since you can get Spec to generate each component of the assignment data – and you can easily tune each generator if you don’t like the default).

Does that help at all?


#6

Definitely helpful. I think the main thing I hadn’t looked enough into was s/fdef, which helps define things. I also appreciate the observation of what my code was providing (a type/validation system to synchronize my front- and back-ends). I think fdef will help me experience more agility, as well as helping me ensure dev-only checking (since it isn’t a public API being developed).