Fork me on GitHub
#sql
<
2021-06-13
>
pinkfrog03:06:50

How to mock a database, for example, with next.jdbc? Do we extend some protocol with test specific methods or ?

seancorfield05:06:49

@i That's a pretty broad question. My recommendation -- generally -- is to use a real DB locally (or in a VM) that you can setup and teardown as needed.

3
seancorfield05:06:18

If you have separated your side effecting code from your pure logic, you can mock the side effecting functions (not the DB itself).

pinkfrog05:06:41

Using a real local DB seems to testing the db connection functionality itself.

pinkfrog05:06:26

Say I have a (save-user-requirements xxx) function, which internally uses (save-to-db! db) function. I need to mock the save-to-db! function into something without communicating with the real db.

pinkfrog05:06:07

I think when I am testing (save-to-db!) function itself, I need use some real db. But when I am testing the (save-user-requirement) function, I need find a way to properly mock out the (save-to-db!) function.

pinkfrog05:06:34

So basically I found (with-redefs) and (defprotocol) to mechanism to alter (save-to-db!), I am not sure which one is more common.

seancorfield05:06:41

Your problem is that your save-user-requirements domain logic function calls a side-effecting function.

seancorfield06:06:14

Separate those and you don't have to worry about "testing the db".

seancorfield06:06:15

What does save-user-requirements do that needs testing (aside from the "trivial" persistence function that shouldn't need testing, right)?

seancorfield06:06:31

If you're going to mix logic and persistence, use a real DB and live with the consequences. Otherwise, properly separate your logic from your persistence and just test your logic.

pinkfrog06:06:52

Yes. I want to go with the second approach. This should save much hassle.

pinkfrog06:06:06

Do you think this is an anti-pattern?

pinkfrog06:06:08

;; trasforms the data
(defn- save-user-requirements-impl [data])

(defn save-user-requirements [data db]
  (-> data
      save-user-requirements-impl
      (write-to-db db)))

pinkfrog06:06:33

If I separate the persistence logic from the (pure) business one. Then all the save-user-requirement-alike function skeleton will look like the above

pinkfrog06:06:23

Kinda like some contrived repetitive code structure, designed in a specific way for the testing purpose only.

seancorfield06:06:39

Right, which should suggest that you need to think more broadly about how you make this separation at all levels.

pinkfrog06:06:03

Yes. I’ve been thinking about a general clojure application layout containing external state interactions (such as db, web) for some time but couldn’t settle down on something natural to me. Do you have any good codebase for reference?

indy10:06:51

What we usually do is to have a real test db like Sean suggests. A lot of times you really need it for end to end tests. We have a macro that will create a transaction from the test-db-spec and wrap the test's body, inside which, you can use this transaction wherever you usually pass a db-spec or transaction or connection. The macro will wrap the body in a try, finally block and in the finally block will rollback the transaction. Example:

(with-test-txn [txn (test-db-spec)]
  (testing "some test"
    (write-answer-to-db txn 42)
    (is (= 42 (get-answer-from-db txn))

pinkfrog15:06:19

@UMPJRJU9E Thanks. The example you give is about testing the (write-to-db) function. How do you test (save-user-requirements) as mentioned in this thread?

indy15:06:05

The save-user-requirements that you broke down looks good. This is usually how I write domain logic for a vertical. The ns usually has one entry function acting as the interface to the outer world, that in turn calls various pure and impure functions. The pure functions can be easily unit tested and the interfacing function is end to end tested with data setup in the test db. Sometimes the impure functions can be mocked like when they're calling external APIs or if it is a DB use a test DB. Since (save-user-requirements) is internally calling the DB, you could with-redef the function that gets you your db-spec to return the test-db-spec instead.

indy15:06:58

But I'd love to hear how really experienced folks like Sean do it.

pinkfrog15:06:32

How do you test the function that invokes the interfacing function, for example, suppose the (my-outer-function) calss (save-user-requirements). Do you with-redef (save-user-requirements) ?

pinkfrog15:06:49

s/calss/ calls

indy16:06:56

You want to test my-outer-function? It depends on your assertions and what you're confident with. If you want to test the side effect of save-user-requirements then you should use a test-db. If you're fairly confident that it will work, you can with-redef it.