Fork me on GitHub
#mount
<
2017-07-12
>
onetom02:07:26

@tolitius The question was really about the function signatures of notify, query & send because those are the functions i would write tests for. I'm not sure what do u suggest because u went from having • a Database protocol with one query method with an implementation accessible as (:db customers) • an EmailSender protocol with one send method with an implementation accessible as (:email customers) to • an app.db state doing the same as (query (:db customers)) • an app.email state doing the same as (send (:email customers))

onetom02:07:02

So let me ask more specifically. Given this implementation:

(in-ns 'app.db)
(defstate db
  :start (db-lib/connect "db-uri")
  :stop (db-lib/disconnect db))

(defn query [name]
  (db-lib/query db some-query name))

; ----------------------------------------

(in-ns 'app.emailer)
(defstate emailer ...)
(defn send [addr msg]
  (email-lib/send emailer addr msg))

; ----------------------------------------

(in-ns 'app.customers)
(require 'app.db)
(require 'app.emailer)

(defn notify [name message]
  (app.emailer/send 
    (app.db/query [name])
    message))

onetom02:07:32

which could be tested as:

(in-ns 'app.customers-test)
(require 'app.customers)
; The test should be aware what other states does the System Under Test depends on
; which would be nice to keep as an implementation detail to make tests less rigid
(require 'app.db)
(require 'app.emailer)

(defn mount-start-stop [test]
  (mount/stop)
  (mount/start-with
    {#'app.db/db           (comment "some mock db implementation acceptable to db-lib/query")
     #'app.emailer/emailer (comment "some mock emailer implementation acceptable to email-lib/send")})
  (try (test)
    (finally (mount/stop))))

(use-fixtures :each mount-start-stop)

(deftest notify-test
  ; GIVEN
  (db-lib/insert app.db/db {:name "Joe" :address ""})
  ; OR using some domain function instead?
  (app.db/add-person "Joe" "")

  ; WHEN
  (let [notify-result (app.customers/notify "Joe" "Hello Joe")]

    ; THEN function return value
    (is (= :ok notify-result))

    ; AND other state changes are
    (let [email (first-sent-email app.emailer/emailer)]
      (is (= "" (:to email)))
      (is (= "Hello Joe" (:body email))))))

(deftest other-notify-test
  ; GIVEN
  "some other db content"
  ...)

onetom02:07:39

I think it's important to show a complete realistic test to really evaluate the consequences of an approach.

onetom02:07:48

but based on your example, I think what u wanted to say was

(in-ns 'app.customers-test)
(require 'app.customers)
; Still need to know the dependent states
(require 'app.db)
(require 'app.emailer)

(mount/start-with
  {#'app.db/query     (fn [name] (println "querying DB with" name))
   #'app.emailer/send (fn [addr msg] (println "sending email to" addr "body:" msg))})

onetom02:07:03

If that's correct then a full example would look like this:

(in-ns 'app.customers-test)
(require 'app.customers)
; Still need to know the dependent states
(require 'app.db)
(require 'app.emailer)

(defn ensure-mount-stop [test]
  (mount/stop)
  (try (test)
    (finally (mount/stop))))

(use-fixtures :each ensure-mount-stop)

(deftest notify-test
  ; GIVEN
  (let [emails (atom [])]
    (mount/start-with
      {#'app.db/query     (fn [name] 
                            (if (= "Joe" name)
                                "" 
                                (str "<address for '" name "''>")))
       #'app.emailer/send (fn [addr msg]
                            (swap! emails conj [addr msg]))})

    ; WHEN
    (let [notify-result (app.customer/notify "Joe" "Hello Joe")]

      ; THEN function return value
      (is (= :ok notify-result))

      ; AND other state changes are
      (is (= 1 (count @emails)))
      (is (= ["" "Hello Joe"]
             (first @emails))))))

(deftest other-notify-test
  ; GIVEN all over again just slightly differently?
  (let [emails (atom [])]
    (mount/start-with
      {#'app.db/query     (fn [name] 
                            (if (= "Mary" name)
                                "" 
                                (str "<address for '" name "''>")))
       #'app.emailer/send (fn [addr msg]
                            (swap! emails conj [addr msg]))})
  ...

onetom02:07:12

What i miss here is some standardized way of setting up the substituted states. some kind of a unified way to describing preconditions to the test. I also feel like there should be a (reset! emails []) in there somewhere but since it's in just local scope now, not inside a state, it works fine this way too. I've also noticed that the two approaches match the 2 major approaches of testing 1. using mocks (when states are some kind data/objects) 2. using stubs (when states are functions)

onetom02:07:27

For the sake of completeness lets also see how the implementation would look like for the test above:

(in-ns 'app.db)
(defstate db
  :start (db-lib/connect "db-uri")
  :stop (db-lib/disconnect db))

(defstate query
  :start (fn query [name]
           (db-lib/query db some-query name))
  ; :stop is not necessary because this state is a pure function which is stateless...
  ; which is kinda confusing to define it as a state just because it depends on a state...
  )

; ----------------------------------------

(in-ns 'app.emailer)
(defstate emailer ...)
(defstate send 
  :start (fn send [addr msg]
           (email-lib/send emailer addr msg))
  ; :stop is not necessary for the same reasons as explained above
  )

; ----------------------------------------

(in-ns 'app.customers)
(require 'app.db)
(require 'app.emailer)

(defn notify [name message]
  (app.emailer/send 
    (app.db/query [name])
    message))

; OR wait a second!!!
; the functions it uses are defined as states so this function also depends on states,
; hence this should be defined as state too, no?
; 
; otherwise it might see older functions referring to stale, already stopped state?...
; okay, here is where im not sure and find it slightly magical :/

(defstate notify
  :start (fn notify [name message]
          (app.emailer/send 
            (app.db/query [name])
            message))

onetom02:07:31

If I must define notify using defstate in order to have a consistent test structure, where I don't have to handle "function states" differently from "value/object/mutable states", then it smells a bit like a whole-app buyin to me...

onetom03:07:10

So my question is Q1. Which approach do you promote and why? 1st approach is wrapping objects into states which are substituted in tests 2nd approach is packaging not just the objects but the functions (which are the APIs of our modules) into states so such API can be substituted during tests (in both cases the functions implicitly reference the states holding objects which are required by their underlying libraries. there is actually an approach where we make these dependencies of our API functions explicit by requiring them as arguments, just like how most underlying libraries do so too)

onetom03:07:15

Q2. Am I correct to say that the difference between the 2 approaches whether the tests for them must be written in a mockist or stubbist style?

onetom03:07:27

Q3. If you recommend the second approach, then should notify be a function or a state?

onetom03:07:01

I think "it can be both" is not an acceptable answer, because it affects how tests should be written for them and that introduces such inconsistency which i feel is not inherent to the problem (of combining states and state using functions)

dm307:07:02

interesting example. I find myself doing the following when using Mount: * states are things which have a clearly defined lifecycle - a DB connection, a stream of data, an atom of things * states are hidden in the declaring namespace. The API of the namespace are the functions operating on states. * tests test the API directly by mocking the functions

(testing ...
  (let [!results (atom [])]
     (with-redefs [my.notifications/notify! #(swap! !results conj %)]
       (my.email/send-with-notifications "IMPORTANT")
       (is (= ["IMPORTANT"] @!results))))
of course the mocking is usually hidden away behind a few functions/macros.

dm307:07:47

(this is a degenerate test that I would not consider writing in real life)

dm307:07:05

start-with has more uses when the namespace API is a shallow wrapper or the test uses an actual testable implementation of the resource captured in the state, like a mem datomic connection.

onetom07:07:16

@dm3 thanks. interesting. i thought a major point of using mount is to avoid with-redefs so im even more puzzled now 🙂

dm308:07:21

why would you try to avoid with-redefs?

dm308:07:36

if that’s your API?

onetom10:07:34

@dm3 https://youtu.be/13cmHf_kt-Q?t=1626 watch it until 29:16 he explains quite precisely what kind of issues will u have if you have any async code within your tested system and some other considerations

dm310:07:04

I know all of that

dm310:07:32

never had to deal with issues like that in tests though

dm310:07:52

maybe because I’m already aware of them

dm310:07:22

I can see how that could lead to problems if running tests in parallel though. Haven’t had the need to do that yet

dm311:07:25

you’d have to use something like Boot’s pods

onetom14:07:51

using pods would be even more ceremony and distraction from the core logic of what you are trying to achieve...

onetom14:07:38

and i don't think the main issue with with-redefs is parallel test running but more like having any kind of asynchrony mixed into the program. an rather realistic example is sending an email out. would you wait for an amazon ses api call to complete during a user registration step before you respond to the user regarding his registration being successful?

onetom14:07:00

if yes, it slows the response down, if no, then how would u know from your test when has that call finished? if u provide a mock ses component which is synchronous, then you are cheating because you are testing a system with different semantics...

dm314:07:05

hmm, are you talking about testing code like

(defn register [email name]
  (db/save! {:email email, :name name})
  (email/send! {:to email, :text (str "hi " name)}))
? I’m not sure I understand the issue… The code calling email/send! doesn’t know whether that’s sync or async. What sort of tests are you thinking about?

dm314:07:20

if email/send! returns a promise then the mocked function would also return a promise

onetom14:07:30

yes, email/send! might just return a promise and that might not even be the return value of the register function, so from a test you would need to be able to wait for the email to arrive

dm314:07:57

I guess I don’t understand why you need to wait for an email to arrive if you mock it out

onetom14:07:19

imagine you are writing a route handler or a castra endpoint. those functions should return a http response (roughly)

onetom14:07:18

so you wont have access to the promise from the tests

dm314:07:55

are we talking about a unit test for a e.g. route handler where you mock out all dependencies?

onetom14:07:37

okay, so i guess that's the source of the misunderstanding. what is a unit test? to me a unit in this case is the register function and i want to test that...

onetom14:07:58

preferably with the least amount of modification compared to the production setup

dm314:07:35

ok, so a unit test would mock out everything and just test the unit logic.

onetom14:07:55

so most of my code should run behind it

dm314:07:16

you mean like stuff inside db/save! and email/send!?

dm314:07:33

ok, that’s not a unit test in my book anymore 🙂

onetom14:07:40

because if i mock those out then im totally coupling my test to the implementation details

onetom14:07:00

which is rather pretty fucking useless in my book 😜

dm314:07:03

that’s why you wouldn’t write a unit test for that fn in real world

onetom14:07:20

then we agree 🙂

dm314:07:38

yeah, but I guess we were working towards some example where with-redefs doesn’t work

dm314:07:02

I use it to mock out APIs, like email/send! or db/save!

onetom14:07:04

so how would you call such a test where you still let code behind db/save! and email/send! run?

dm314:07:45

I’d have to with-states the actual things inside defstate with testable implementations

onetom14:07:06

we are going off-topic though... maybe #testing would be a better channel for such discussion 🙂

onetom14:07:30

i agree with that statement too

onetom14:07:19

however the "cheating" enters the equation when we start to use defstate on the API functions around the stateful constructs of our program...

onetom14:07:09

i think the problem is that we don't have a clear, consistent, shared vocabulary around the topic

dm314:07:36

in my world you either mock the API function for a unit test or use a testable stateful thing for an integration test

onetom14:07:47

dm3: i would actually call the "testable stateful thing" case mocking and the fake function case stubbing

onetom15:07:52

and those two styles was i trying to show in my examples earlier

dm315:07:22

hmm, I always thought of stubbing - returns a default value/does nothing; mocking - responds to complicated interactions, remembers how many times it was called with what arguments, etc

dm315:07:29

the “testable stateful thing” is more like an in-memory db instead of a file-based one

onetom15:07:50

well, you heard about mock objects but not stub objects right?

dm315:07:31

does my definition disagree with what stub objects are? Wiki says > A Stub Object is one whose methods are stubs (or “mocks”?); that is, they do no useful work but prevent #doesNotUnderstand errors and return plausible values so that the computation can continue.

dm315:07:57

haha >(or “mocks”?)

onetom15:07:37

because currently it's just you who are saying it 😜

onetom15:07:34

It's like as if I'm saying: Wiki says > A Stub Object is something which @dm3 should use more often ;D

dm315:07:41

I think this is the best explanation: https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs. The one I quoted is from c2 wiki

onetom15:07:53

yeah, thats what i consider the best source too and thats why i said earlier > we don't have a clear, consistent, shared vocabulary which the article expresses as: > The vocabulary for talking about this soon gets messy - all sorts of words are used: stub, mock, fake, dummy.

onetom15:07:31

btw, that Gerard Meszaros he is referring to was also a Hungarian guy, like myself 😉

onetom15:07:10

and mészáros means butcher 😉

dm315:07:34

Did any of your questions get answered though?

onetom15:07:48

so maybe we should agree then on not using any of these dummy, fake, stub, spy, mock terms, just test double instead

onetom15:07:26

since we are not even talking about object oriented code strictly

onetom15:07:00

our stateful constructs though are still objects in "reality" and they should be differentiated from the functions working with them. there are two distinct ways how functions can use such objects: • explicitly depending on them, meaning they receive them as parameters • implicitly depending on them, meaning they have a reference wired into them at compile time

onetom15:07:19

and no, i don't think we got any clearer, since i asked 3 quite specific questions and i didn't get 3 corresponding specific answers

onetom15:07:47

regarding with-redefs im on the same view as expressed here: https://www.reddit.com/r/Clojure/comments/4wrjw5/withredefs_doesnt_play_well_with_coreasync/ > So IMO, the solution is simple. Don't use with-redefs. Any time you see "with-redefs" think "nasty hack" in your mind.

dm315:07:38

well, my answers are: Q1 - 1st approach for integration tests, mocking API functions for unit tests Q2/Q3 - haven’t seen anyone use states as functions so no comments

dm315:07:15

and I like my redefs 🙂

dm315:07:04

I also don’t use core.async

onetom15:07:43

the master of mount just gave an example of using functions as states: https://clojurians.slack.com/archives/C0H7M5HFE/p1499782555817759

onetom15:07:04

and his article he referenced also gives such an example in form of the #'app.sms/send-sms state https://clojurians.slack.com/archives/C0H7M5HFE/p1499782582834684

dm315:07:19

oh, right 🙂 well, I haven’t had the need to do that

dm315:07:49

so I won’t be able to help

onetom15:07:18

that's fine, but you can't say anymore that you haven't seen anyone using that approach 😉

dm314:07:33

but I must confess that I do less testing than I probably should

tolitius17:07:09

@onetom: If I understood your questions correctly I believe the confusion is around the actual implementation vs. testing.

tolitius17:07:14

In case states extend protocols, their stubs can be reified in start-with to a new behavior that would be useful for testing

tolitius17:07:19

In case states are functions they can be simply replaced with testing functions in start-with. EmailSender is such an example. I would have a state to be a function that is able to send emails (similar to an "sms sender" example).

tolitius17:07:25

In case states are IO, resources, etc.. objects (such as a database "conn"ection) and you'd like to test the code that uses them, I would only access them via functions that take them as arguments. For example, I would not do this:

tolitius17:07:31

(defn query [query params]
  (jdbc/fetch conn query params))

tolitius17:07:35

but would rather do this:

tolitius17:07:40

(defn query [conn query params]
  (jdbc/fetch conn query params))

tolitius17:07:45

in which case you can either stub or mock a conn state. i.e. usually an in memory db or you could mock conn object as well of course.

tolitius17:07:09

> which approach do you promote and why?

tolitius17:07:14

I don't like to promote an approach 🙂 I believe there is no one approach that works for all. I would personally avoid creating states for higher order constructs such as a query function above, and would only have a handful of states that are as low level as possible: sockets, db connections, thread pools, etc..

tolitius17:07:19

from your example above I would keep notify, query, send and other functions as just functions and would only have two states: email-sender and conn. Functions that rely on states would take them as arguments.

onetom17:07:37

so as a consequence, what would be the signature of the notify function? 1. (notify [db emailer addr msg]) 2. (notify [emailer db addr msg]) 3. (notify [[db emailer] addr msg]) 4. (notify [{:keys [db emailer]} addr msg])

tolitius18:07:29

would really depend on a way you'd prefer to structure your application / functions. from your example above it seems notify does two things: * find an email address in a database by a user id * sends a message to the email address so something like:

(-> (select conn query)
    (send-email msg))
it could either be used as ^^ in case your local scope where you call it from has access to conn and send-email, or if you call it from multiple places it could look something like this:
(defn find-email [conn id]
  (jdbc/select conn ... 'id' ))

(defn notify [send-email conn id msg]
  (-> (find-email conn id)
      (send-email msg)))