A Thought on Writing Tests That Suck Less

 

I visited the ClojureD conference in Berlin on 24.2.2018 during some cold winter days. I went with my colleagues to gather some insights and listen to interesting talks on different subjects around Clojure. As a result I decided to challenge myself (and tests I write), inspired by Torsten Mangners good presentation titled:
Writing tests that suck less – “What” vs “How”.
Disclaimer: This post takes no credit whatsoever on the subject or claims to be a best way of doing things. This text relies on Torsten Mangners presentation but there is also some personal interpretation.

The Gauntlet

As Torsten reasoned that writing pure unit tests in Clojure (or any other language) is usually a small task. You invoke your unit with inputs and assert the results. Tests ideally document our software (as an added side-effect of verification).
Let’s see a simple, self-documenting unit tests.
;; the implementation (the unit, the HOW)
(defn add-numbers
  [numbers]
  (apply + numbers))

;; the test that should describe to a (technical) reader WHAT we are testing
(deftest add-two-numbers-together
  (testing "If two numbers can be added together"
    (is (= 3 (add-numbers '(1 2))))))
So it’s pretty straightforward, we don’t need to know the HOW or any other implementation details and because we are testing a pure unit with no dependencies we don’t need to prepare anything obfuscating and arcane.
Writing tests should not be an effort for a developer – but it starts to feel so when we run out of pure functions and go over to integration testing. It shouldn’t be too hard to write the actual tests – we just end up with a bunch of namespace_test.clj(s) files that can end up being many times longer than the actual code including a lot of HOW:s. Arcane and magical ceremonies, where we roll out mocks, stub and whatnot – finally ending in one or a few strange assertions.
If you jump into a project and find out that the documentation basically is the source code, you would be glad to find out that the tests would clearly tell us WHAT they test and prove. We don’t want to investigate and reverse-engineer the HOW part. We could just read what the software internals in it’s current state is documented and supposed to do.
At some point in time, as a software developer, you would need to know the HOW but that’s more of the part of writing the implementation (and the tests).

Throwing The Gauntlet

Thus I challenge myself to separate the bloat of HOW out my tests that repeatedly ended up looking like this:
(deftest handler-post-to-user-success
  (testing "calling handler posting message to user"
    (async done
      ; channel open
      (.mockOnce xhr-mocklet "POST" #".*"
        (mocked-response (clj->js {:ok true}) 200 "application/json"))
      ; post
      (.mockOnce xhr-mocklet "POST" #".*"
        (mocked-response (clj->js {:ok true}) 200 "application/json"))
      (go
        (let [response (<! (api/handler 
          {:body {"data" {"message" "testmessage"
                          "user-id" "user-id"}}}))]
          (is (utils.http/request-success? response)))           
          (is (= "post message to channel success" 
                 (-> response :body :message))))
    (done)))))

This test mocks two successful HTTP calls, invokes the handler (entry point of the serverless function) and performs some assertions.

For those who are curious about what’s going on in here – this is a test for a serverless REST API handler in a Slack Bot backend. It’s written in ClojureScript and runs on Javascript function runtime on Azure.  See Siili Solutions / Hedge on GitHub – a serverless framework to deploy ClojureScript functions on Azure and AWS.

This test checks how our serverless API responds to clients if it succeeds when contacting the Slack API.

So is this test complex? Maybe not too complex. But when you test more complicated logic it probably will be. Also there’s lot of potential arcane ceremony that doesn’t add to test readability like async done, (done), go, <! that is purely about the HOW in my tests. And those things just repeat over and over, multiple times per test namespace.

The Dust Settles

So this was my quick try to find a general, more WHAT for my tests.

(deftest handler-post-to-user-success
  (testing-handler "calling handler, posting message to user succeeds"
    :incoming-request {:body {"data" {"message" "testmessage"
                                      "user-id" "user-id"}}}
    :ext-http-calls-return #(do (channel-open-success)
                                (channel-post-success))
    :assert #(do (is (utils.http/request-success? %))
                 (is (= "post message to channel success"
                        (-> % :body :message))))))

Maybe it wasn’t state of the art of perfect, but I hope you see the intent. So the summary of the WHAT in my case:

  • We are testing the handler (the serverless functions entry point)
  • The input values are presented in :incoming-request
  • :ext-http-calls-return describes what is going on in the “external” dependencies during this run (mocks are instantiated)
  • :assert contains the assertions that should be done for this test

And therefore:

  • Less noise
  • Less boilerplate
  • More documentation

So I re-wrapped testing to do the things that are repeated over and over in all the tests:

(defn testing-handler [message & {:keys [ext-http-call-returns
                                         incoming-request
                                         assert]}]
  (testing message
    (async done
      ;; install mocks
      (ext-http-call-returns)
      (go
        ;; invoke the handler
        (let [response (<! (api/handler incoming-request))]
          ;; assert
          (assert response)
    (done))))))

And my mocks are just a different combination of successes and failures, so they were just wrapped into their own functions.

Conclusion

You could still reason that this could even be more simplified. Macros could help me to get rid of even more noise. But in the end of the day,  Torstens presentation at ClojureD  made me more motivated about trying to write tests that suck less. Or die trying.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s