marick/Midje

Failure of test of com.stuartsierra.component/system-map, but no diff is shown

Closed this issue · 4 comments

I'm new to both Component and Midje, so I apologize for any naivete inherent in this question.

I want to test that my components and systems are getting passed the correct arguments to their constructors, because I have some abstraction on top of their constructor calls. This works just fine for my components, but when I try it against my system, the tests fail but produce blank diffs. Here's some stripped down code that reproduces the issue:

(ns clojure-test.core
  (:require [midje.sweet :refer :all]
            [com.stuartsierra.component :as component]))

(defrecord ExampleComponent [arg1]
  component/Lifecycle
  (start [this])
  (stop [this]))


(defn example-system [arg1]
  (component/system-map
   :example-component (->ExampleComponent arg1)))

(facts "about `ExampleComponent`"
  (fact "it can be instantiated and its arguments are properly passed to its constructor. [THIS PASSES]"
    (->ExampleComponent "test") => {:arg1 "test"}))

(facts "about `example-system`"
  (fact "it can be instantiated and its arguments are properly passed to its components [THIS FAILS]"
    (example-system "test") => {:example-component {:arg1 "test"}}))

And the output:

======================================================================
Loading (clojure-test.core clojure-test.core-test)

FAIL "about `example-system` - it can be instantiated and its arguments are properly passed to its components [THIS FAILS]" at (core.clj:22)
    Expected: {:example-component {:arg1 "test"}}
      Actual: {:example-component {:arg1 test}}::com.stuartsierra.component.SystemMap
       Diffs: 
>>> Output from clojure.test tests:

0 failures, 0 errors.
>>> Midje summary:
FAILURE: 1 check failed.  (But 1 succeeded.)

Is this my fault, or a bug?

Interestingly, if I use explicit checkers as in the following, everything works:

(facts "about `ExampleComponent`"
  (fact "it can be instantiated and its arguments are properly passed to its constructor"
    (->ExampleComponent "test") => (every-checker (contains {:arg1 "test"})
                                                  #(instance? ExampleComponent %))))

(facts "about `example-system`"
  (fact "it can be instantiated and its arguments are properly passed to its components"
    (example-system "test") => (every-checker (contains {:example-component {:arg1 "test"}})
                                              #(instance? SystemMap %))))

I'll go with this for my workaround. Still smells like a bug to me, but you might think otherwise.

So in general Midje should be able to compare records with maps: https://github.com/marick/Midje/wiki/Checking-maps-and-records

(def sm (component/system-map :e "foo"))
(fact sm => {:e "foo"}) ;; => true

That said, your SystemMap record here is nested, in that it has an ExampleComponent inside of it. For some reason Midje doesn't know how to deal with this.

On the otherhand, nesting a ExampleComponent inside of a ExampleComponent is fine:

(def ec (->ExampleComponent (->ExampleComponent "a")))
(fact ec => {:arg1 {:arg1 "a"}}) ;; => true

Let's keep this open and when I get a chance I'll try to dive deeper into it.

Sounds good. As an additional datapoint, I had noticed that the maps print out with a ::ClassName at the end, so I thought originally that I could tack on ::ExampleComponent at the end of my checker's map to also check the type. (In other words, I thought I was getting the #(instance? ExampleComponent %) check for free with minimal syntax.)

But when I tried changing the class name to something wrong, everything still passed. So you can include the ::ExampleComponent at the end and it won't fail, but it doesn't do anything for you either.

It might be nice to actually do an instance? check behind the scenes when you specify the ::ClassName at the end, which would mirror the output format and give you some nice syntactic sugar.

It looks like the whole ::ExampleComponent thing is an artifact of Midje. I'll update Midje to follow the normal behavior of printing records with the disptach macro like #ClassName {:value-one 1} to avoid confusion.

I think I was able to track down the original issue to how Midje converts records into maps to do comparison.
It won't walk nested structures and convert any nested records into maps (see here), and hence the check comparing nested records with nested maps fails.
I'm a bit weary of changing the core equality code to address this. I'm going to continue mulling it over, but close this issue out.

A work-around is to use a checker like contains or just (examples here). For whatever reason, they are able to handle this type of thing. For example:

(defrecord Inner [inner-value]
  Object
  (toString [this] inner-value))

(defrecord Outer [outer-value]
  Object
  (toString [this] outer-value))

(fact "this won't pass"
  (->Outer (->Inner 1)) => {:outer-value {:inner-value 1}})
(fact "this passes"
  (->Outer (->Inner 1)) => (just {:outer-value {:inner-value 1}}))