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.

Why should I choose functional paradigm?

Choosing a functional paradigm language has been a hot topic for a while. Functional vs. Object Oriented Paradigm is a constant discussion topic within the developers.

Before thinking the benefits of FP, the working environment must be suitable for modern work

The key of success at the management point of view is not to micromanage. Let the team choose their working methods and tools. The project is always a compromise between time, features and given resources. It’s all about how the project management sees that triangle.

Every tool and way to work has their own place. Telling the developers HOW to do work instead of WHAT to do is always a problem. It kills creativeness and get the authority outside the dev team. Too often the author is someone who doesn’t know about the actual developer work.

Treat the professional developers with this Sledge Hammer principle: “Trust me, I know what I’m doing”. Developers should know exactly how the program works, because code is the most detailed specification of the solution. It’s all about communication between the people – and between the human and the computer.

When the environment is suitable for working, then we can discuss about the benefits of FP

So why should I choose FP instead of OOP/imperative? What benefits it would bring the table? Here’s some thinking I’ve done in the past years based on my experiences:

Functional programming makes especially list handling a way easier. There are effective functional languages like Clojure or Scala. Another option is to use functional libraries like Ramda or Lodash. I like flexible code, so I prefer using scalable languages, like ES6, Scala, Java8 or C#/LINQ. With OOP and FP combined I have more options available. It’s also a safe choice when there ain’t so much pure functional programmers in the team (yet). Scaling the Object Oriented code with functional flavour is understandable compromise with functional averages like me. Actually I’m on my way of using only functional languages.

Here’s some benefits I found on functional paradigm which could save some money

It’s stateless and then also immutable, so no side effects

Object Oriented Paradigm is all about sharing the lifecycle between the objects. OOP makes problem solving often too complex, and complexity costs. Mutable state makes side effects possible, so code is harder to test with a full coverage. Developing with FP forces to code small functions. Simplified syntax (especially with pure FP languages) gives more time to think the actual business logic. Focusing on one thing at the time is clever.

When the state is immutable, it’s way easier to scale up and still control the whole

When programming high capacity systems, it’s crucial to design the software architecture to be scalable for parallel processing. If the program isn’t handling any state inside the runtime, it’s way easier to scale by using micro services or FaaS container platforms like AWS or Azure. Then the runtime isn’t blocked to serve only one client for a long time – there’s more time to handle many short tasks instead of a one big blocker.
So no more if-else-if-else or switch-case spagetti, or recursive for loop hell. Only function chains like map, filter or reduce one-liners.
It might be hard to transform the brain in the right mode after coding years with OOP. Functional code readability might cause many WTFs in the beginning, but – trust me – It’s worth of it. Like Twelve-factor manifest says “Execute the app as one or more stateless processes“. Start by one and try to write more if it feels right and suitable for the context.
Learning functional programming makes you a better OOP developer. You don’t have to be an expert, but it’s beneficial to learn something new. It’s hard at the beginning, but with the supportive team it’s possible to achieve.
Functional languages are simple by design when you let it be so.

Here’s a recap of my points:

  • Let the developers to choose the right tools and processes for building the high quality solution
  • As a team member, encourage the colleagues at least try to code with FP
  • Functional code is simple and easy to maintain. when it works, it works (no side-effects like in OOP). Broken code is easier to recognise and refactoring is less risky operation
  • Data must persist outside the code (like in Redux architecture). It shortens or eliminates debugging marathons. Less risk for wasting the time hunting the bugs from the production environment

See also: