Test sending emails with protocols

How do you test your system when it needs to send an email?

Imagine a working on a story card tracker, like pivotal or trello. A user can be assigned a card. Then the system should send them an email and let them know. Wait a minute … Our code needs to talk with the outside world!

How do we test its going to send the right email?

Lets gather some example code. A user can be assigned a card with the app.core/assign-card function and it needs to send them an email.

(ns app.notify
  (:require [postal.core :as postal]))

(defn user-assigned! [mail-host email title]
  (postal/send-message
    mail-host
    {:from "[email protected]"
     :to email
     :subject (str title "has been assigned to you")
     :body "......"))
(ns app.core
  (:require [app.notify :as notify]))

(defn assign-card [config user card]
  ;; do some other work
  ;; then notify the user
  (notify/user-assigned! (:mail-host config)
                         (:email user)
                         (:title card)))

We'd like to write a test checking that app.core/assign-card sends an email to the right address. Unfortunately the side effecting code is further down in the system. It's in another namespace and referred to through notify/user-assigned!.

The app.notify namespace can be thought of as a global singleton. It comes with all the advantages of global singletons, but also has the downsides. The important downside for testing is the difficulty of replacing it in order to change the functionality. We'd like to be able to refer to the function, but then call some stub implementation that doesn't actually send an email.

Using a protocol allows us to switch the functionality

Using a protocol allows us to refer to the function, but pass in an argument that determines which implementation gets used. We can then create one implementation that uses the original functions to send email.

The actions required to replace the namespace with a protocol: 1. Create a protocol with methods for each function used outside the namespace, ignoring any config arguments 2. Create a record that will implement that protocol through the current functions, storing the config arguments in the record 3. Change any callers of those functions to use the new protocol methods and call with a record instead of any config arguments

In the case of sending an email, we can make a Notify protocol. Then we create a record PostalNotifier that takes the mail-host config. Finally we change the system's config to use the new record.

(ns app.notify)

(defprotocol Notify
  (user-assigned! [this email title]))

(ns app.notify.email
  (:require [postal.core :as postal]
            [app.notify :as notify]))

(defrecord PostalNotifier [mail-host]
  notify/Notifier
  (user-assigned! [_ email title]
    (postal/send-message
      mail-host
      {:from "[email protected]"
       :to email
       :subject (str title "has been assigned to you")
       :body "......"))
(ns app.core
  (:require [app.notify :as notify]
            [app.notify.email :as email))

(comment
  ;; production config should now contain a
  (email/->PostalNotifier mail-host)))

(defn assign-card [config user card]
  ;; do some other work
  ;; then notify the user
  (notify/user-assigned! (:notifier config)
                         (:email user)
                         (:title card)))

Create a stub implementation for the protocol

Adding this abstraction allows us to create a stub implementation of Notify. We can then create a config for testing that uses the StubNotifier and use that in our tests. Given this is a stub and built for testing purposes, we can even reach inside to grab data and examine it for correctness.

(ns app.core-test
  (:require [app.core :as app]
            [app.notify :as notify))

(defrecord StubNotifier [emails]
  notify/Notifier
  (user-assigned! [_ email title]
    (swap! emails conj {:email email :title title})))

(deftest assigning-a-user-sends-them-an-email
  (let [notifier (->StubNotifier (atom []))
        user ....
        card ....]
    (app/assign-card {:notifier notifier}
                     {:email "email"}
                     {:title "title"})
    (is (= (:email (first (:emails notifier)))
           (:email user)))))

Most of the time using normal namespaced functions is great. Occasionally we'll build some namespaces that are designed for side effects. In order to test the overall system we'll want to replace that part, and a protocol will work out better. It allows us to provide stub implementations, and make sure that their functionality is being called as desired.