Fork me on GitHub
#clojure-spec
<
2017-01-09
>
pyr09:01:23

does anyone have an example showing how they wired generative tests in their test suite?

pyr09:01:49

I'm using exercise-fn and instrument at the repl but wanted to know if there was a standard way of wiring that in tests

mpenet09:01:30

I guess there are a few ways to do this: one would be just to call instrument on "normal" tests, then another could be replacing your test values with generated versions of it, which might or might not be difficult depending on what it is.

mpenet09:01:14

I haven't done any of it so far personally. Well actually just the instrumentation part on 1 project.

mpenet09:01:44

clj jdbc and alia both have instrumentation on in the test suite for instance

pyr09:01:39

cool, will look at that, thanks!

lmergen09:01:18

so, as an alternative of calling s/explain on all pre or post conditions, what about calling s/assert rather than s/valid ?

lmergen09:01:44

e.g.

(defn new-web-server [config]
  {:pre [(s/assert ::config config)]}
  (-> (map->WebServer config)
      (using [:routes])))

lmergen09:01:26

i know the semantics aren't very clean (:pre is supposed to return true/false rather than an exception), but it does contain a lot more information

pyr10:01:35

@mpenet Integrating with the output of check works well

pyr10:01:55

How do you go about generating fixed size arrays quickly, usually?

mpenet10:01:45

depends if you mean primitive array or vector/list

pyr10:01:32

primitive in this case, but the cheatsheet will help, thanks again

mpenet10:01:24

you can generate a list of ints then feed it in byte-array in a gen/fmap for instance

mpenet10:01:37

there are probably easier ways

pyr10:01:30

(tgen/resize 64 (gen/bytes)) does the trick

pyr11:01:08

@mpenet (actually not at all, so I'm left with a gen/bind on top of a vector)

mpenet11:01:29

I had something like this in mind: (gen/fmap byte-array (gen/vector gen/byte 10))

mpenet11:01:15

probably very similar to what you have

mpenet11:01:35

a gen/bytes* with a size argument would be nice tho

pyr11:01:51

@mpenet this is what I have, yes

pyr11:01:28

(except there's no gen/byte)

mpenet12:01:27

there is in clojure.test.check/gen

pyr12:01:41

indeed, that's much better

pyr12:01:05

then what you have works out of the box and is much shorter than what I had

mpenet12:01:40

clojure.spec aliases some of test.check but not all of it

mpenet12:01:10

I guess the idea is to (maybe) someday promote most of it in spec itself, but I could be wrong

mpenet12:01:10

test.chuck is also awesome for more gen utils

mpenet12:01:46

string-from-regex for instance was quite the time saver

pyr12:01:42

A last question for now, is it possible to specify custom generators only for clojure.spec.test/check

pyr12:01:23

It's only a matter of visual preference, to be able to have simple specs in my main namespaces and specs with generators for tests

mpenet12:01:30

Yup, I think so, test/check takes a number of options for this

gfredericks12:01:39

I think that means you want to use overrides

pyr12:01:04

@gfredericks yes, but I only see support for those in exercise and exercise-fn

pyr12:01:31

i'd like c.s.t/check to use them

pyr12:01:48

i misread the doc

pyr12:01:17

it seems as though :gen does this for check

pyr12:01:26

Ah, so the map given to :gen in c.s.t/check is shallow

pyr12:01:04

i.e: the generator overrides are only taken into account for the called fn. If the fn depends on other functions, they won't use the provided generators

gfredericks12:01:33

like for stubbing in particular?

pyr13:01:29

@gfredericks not sure I understand

gfredericks13:01:11

I just don't know why else, when using c.s.t/check, functions other than the one being tested would use generators

pyr13:01:48

you're right i'm not articulating my pb properly

pyr13:01:05

it's when generators rely on other overrided generators that i run into the issue

pyr13:01:24

on of my generators does:

pyr13:01:00

(def map-gen (gen/bind (s/gen (s/keys :req [::a ::b ::c]) mangle-and-return))

pyr13:01:54

if i do (c.s.t/check map-test :gen {::map mag-gen ::a a-gen ::b b-gen})

pyr13:01:16

the generation of ::a and ::b in map-gen will not use overrides

pyr13:01:38

so I have to provide overrides in the s/gen call in map-gen too

gfredericks13:01:39

spec uses functions that return generators in a lot of places

gfredericks13:01:17

Which can defer resolution, I assume. But maybe that can't help here.

pyr13:01:40

it's not a big thing

pyr13:01:54

i just need to specify my overrides in a couple of places instead of once.

gfredericks13:01:23

that seems unfortunate

pyr14:01:47

@gfredericks @mpenet thanks for your help today

schmee14:01:42

whoa, great write-up!

pyr14:01:56

I struggled to decouple the generators from the spec because otherwise it makes things harder to follow. My first iteration had with-gen on the specs themselves and that made it much harder to explain what was going on

lmergen14:01:46

so, if i have a defrecord for which I want to fdef and instrument certain functions defined within that record, how am I supposed to be referring those functions ?

lmergen14:01:05

(i think this is more related to clojure in general, and what the symbol names for functions inside records are)

schmee14:01:13

AFAIK all record functions must be specified with protocols, so you can spec the protocol methods

pyr15:01:38

@lmergen behavior is implemented on top of records through protocols, so as @schmee mentioned, you can specify protocol signatures as you would do functions

pyr15:01:53

(defprotocol Encoder (encode [this])) (defrecord A [name] Encoder (encode [this] (str name)))

pyr15:01:09

if you were to have the above protocol and record

pyr15:01:47

you could specify encode with (spec/fdef encode :args ... :ret ... :fn ...)

lmergen15:01:30

but that would do it for all instances of Encoder, right ? not just of A ?

pyr15:01:49

@lmergen in this case, (spec/fdef encode :args (s/cat :encoder #(satisfies? Encoder %)) :ret string?)

pyr15:01:30

indeed, this is for Encoder in general

pyr15:01:48

but that's how protocols work

lmergen15:01:12

so my concrete problem at the moment is Component

lmergen15:01:30

and ensuring that the start function of certain components return a specific type

lmergen15:01:57

so if i have 5 different components (all using defrecord and the Lifecycle protocol), i want 5 different fdefs

lmergen15:01:07

for those 5 different start function implementations

pyr15:01:55

the expected behavior of start in Lifecycle is that you return the same record possibly augmented with additional fields

pyr15:01:05

so you could write a generic spec

pyr15:01:21

(def component? #(satisfies Lifecycle %))
(spec/fdef start :args (s/cat :component component?) :ret component? :fn (= (.getClass (:component %)) (.getClass (:ret %))))

pyr15:01:29

or something to that effect

pyr15:01:43

unless you're doing something funny with component

pyr15:01:44

If you are really intent on spec'ing per-type behavior for your protocol, there is another much kludgier way. extend (http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/extend) takes a map of protocol implementation for types, which takes fns as args. You could implement Lifecycle this way and then spec the individual functions you have in the map

pyr15:01:09

But again, it seems as though this should not be necessary for Lifecycle

lmergen15:01:30

i understand

lmergen15:01:36

i'm probably solving this problem at the wrong level

lmergen15:01:07

and should probably verify whether my entire system satisfies a certain spec, after it has been initialized

Alex Miller (Clojure team)17:01:03

I don’t think it was mentioned above, but you cannot spec protocol functions

lmergen17:01:05

I suspect that it's an implementation issue, since protocol functions have very different function signatures

hiredman17:01:31

and previous comments have some discussion of why that is

hiredman17:01:29

basically certain kinds of "function" calls(including protocols) are compiled differently and spec just handles the generic case

schmee17:01:36

what the recommended workaround?

schmee17:01:46

wrap it in a regular function and spec that instead?

lmergen17:01:51

that's what I ended up doing yes

Oliver George21:01:58

Am I missing a cleaner way to write this. Goal is to check that the options list has the required keys which are also declared in the data)

(s/def ::Select.props
  (s/and
    (s/keys :req-un [::value ::options ::value-key ::display-key ::on-change]
            :opt-un [::placeholder ::disabled ::loading])
    #(every? (fn [option] (contains? option (:value-key %))) (:options %))
    #(every? (fn [option] (contains? option (:display-key %))) (:options %))))

Oliver George21:01:18

This won't produce very specific errors

Yehonathan Sharvit21:01:22

From today, you can easilly share your clojure.spec related code snippets on klipse - it loads pretty fast

joshjones21:01:19

@olivergeorge so ::Select.props describes a map, one of whose keys is :options, which is a list of maps, each of which must contain values referred to by keys named :value-key or :display-key, which are in ::Select.props -- is that right?

Oliver George22:01:13

So I really want to confirm the options based on the map data

Oliver George22:01:28

Like this movie inception

carocad22:01:31

@kenny I gave it a try at simplifying your macro and came up with this:

(defmacro defspec-test
  ([name sym-or-syms] `(defspec-test ~name ~sym-or-syms nil))
  ([name sym-or-syms opts]
   `(t/deftest ~name
      (let [check-results#  (clojure.spec.test/check ~sym-or-syms ~opts)]
        (doseq [result# check-results#]
          (t/is (nil? (:failure result#)) (clojure.spec.test/abbrev-result result#))
          (when (nil? (:failure result#))
            (println "[OK] - " (clojure.spec.test/abbrev-result result#))))))))
It can be simplified even more but I like the result so far. Hopefully it can be useful to somebody 🙂

kenny22:01:24

You want to use do-report, not println otherwise you'll break integrations with clojure.test

carocad22:01:06

why? I dont know much about clojure.test actually 😕. I figured that the call to t/is already took care of the integration. doesnt it?

kenny22:01:32

The other important difference is that this is counting every single generative test instead of grouping them as one

kenny22:01:56

is will handle it but you will get confusing results for when tests fail

carocad22:01:11

ah I see what you mean. true the output is not as clean as with a normal test since you get the generated symbol but I still find the output better since in the previos macro I was only getting the stacktrace of an error. No idea which function failed nor other info. I dont know if it was a problem in my project though

joshjones22:01:23

@olivergeorge unfortunately, I think what you have is about as straightforward as you can get with what you want. While spec can nest sequential and associative structures easily, there is not another way (that I'm aware of) to relate various levels other than another predicate which is what you've done. If anything, I might combine the two additional predicates into one function, though you may find it gives you an undesirable level of detail:

(fn [{:keys [value-key display-key options]}]
      (every? #(and (contains? % value-key)
                    (contains? % display-key))
              options))