Paul Evans
Paul Evans

Reputation: 329

Testing to the interface, in Clojure

In the Java world, when it comes to developing unit tests, I've followed an approach of "testing to the interface." What that means is, if I have a Java interface, I would write a single unit test class (extending from JUnit's TestCase or whatever) for that interface; to test that interface. This class would be abstract, and would contain a series of test methods for testing the methods of my interface. Here's a quick example:

/** my interface */
public interface MyFooInterface {
    int foo();
    String bar();
}

/** some implementation */
public class MyFooImplA implements MyFooInterface {
    public int foo() { ... }
    public String bar() { ... }
}

/** some other implementation */
public class MyFooImplB implements MyFooInterface {
    public int foo() { ... }
    public String bar() { ... }
}

/** my test case for my interface */
public abstract TestMyFooInterface extends TestCase {

    private MyFooInterface objUnderTest;

    public abstract MyFooInterface getMyFooInterface();

    public void setUp() {
        objUnderTest = getMyFooInterface();
    }

    public void testFoo() {
        ... bunch of assertions on 'objUnderTest'...
    }

    public void testBar() {
        ... bunch of assertions on 'objUnderTest'...
    }
}

/** concrete test class, with very little work to do */
public TestMyFooImplA extends TestMyFooInterface {
    public MyFooInterface getMyFooInterface() {
        return new MyFooImplA();
    }
}

/** another concrete test class, with very little work to do */
public TestMyFooImplB extends TestMyFooInterface {
    public MyFooInterface getMyFooInterface() {
        return new MyFooImplB();
    }
}

So here we have a great thing. No matter how many implementations of MyFooInterface we have, we only need to write 1 set of unit tests (in TestMyFooInterface.java) to ensure the contractual correctness of MyFooInterface. Then, we simply need a concrete test case for each interface implementation we have. These concrete test cases are boring; all they need to do is provide an implementation of 'getMyFooInterface'; and they do this simply by constructing the correct implementation class. Now, when I run these tests, every test method in TestMyFooInterface will be invoked for each concrete test class. By the way, when I say 'when I run these tests', what that means is, an instance of TestMyFooImplA will be created (because it's a concrete test case found by the test harness; something Ant-based or Maven-based or whatever) and all of its 'test' methods will be run (i.e., all the methods from TestMyFooInterface). TestMyFooImplB will also be instantiated, and its 'test' methods will be run. Bam! We only had to write a single set of test methods, and they'll be run for every concrete test case implementation we create (which only takes a couple lines of code!)

Well, I want to mirror this same approach in Clojure when it comes to Protocols and Records, but I've stumbled a bit. Also, I want to validate if this approach is even reasonable or not in the Clojure universe.

Here's what I have in Clojure so far. Here's my "interface":

(ns myabstractions)

(defprotocol MyFooProtocol
    (foo [this] "returns some int")
    (bar [this] "returns some string"))

And now I might have 2 different implementations of this protocol, in the form of records. Here's one implementation:

(ns myfoo-a-impl
    (:use [myabstractions]))

(defrecord MyFooAImplementation [field-a field-b]
    MyFooProtocol
    (foo [this] ...impl here...)
    (bar [this] ...impl here...))

And one other implementation:

(ns myfoo-b-impl
    (:use [myabstractions]))

(defrecord MyFooBImplementation [field-1 field-2]
    MyFooProtocol
    (foo [this] ...impl here...)
    (bar [this] ...impl here...))

So at this point, I'm sort of in the same position I was in my familiar OO Java world. I have 2 implementations of my MyFooProtocol protocol. Each implementation's 'foo' and 'bar' functions should be obeying the contract of the functions as documented in MyFooProtocol.

In my mind, I only want to create a set of tests once for 'foo' and 'bar', even though I have multiple implementations, just as I did in my Java example. So here's what I did next with my Clojure code. I created my tests:

(ns myfooprotocol-tests)

(defn testFoo [foo-f myFoo]
    (let [fooResult (foo-f myFoo)]
      (...some expression that returns a boolean...)))

(defn testBar [bar-f myBar]
    (let [barResult (bar-f myBar)]
      (...some expression that returns a boolean...)))

Great, I've written my tests only once. Each function above returns a boolean, effectively representing some test use case / assertion. In reality, I'd have many, many more of these (for each assertion I'd want to do). Now, I need to create my "implementation" test cases. Well, since Clojure is not OO, I can't really do what I did in my Java example above, so this is what I was thinking:

(ns myfooATests
    (:use [myfooprotocol-tests :only [testFoo testBar]])
    (:import [myfoo_a_impl MyFooAImplementation])
    (:use [abstractions])
    (:require [myfoo-a-impl])
    (:use [clojure.test]))

(deftest testForFoo []
    (is (testFoo myfoo-a-impl/foo (MyFooAImplementation. 'a 'b))))

(deftest testForBar []
    (is (testBar myfoo-a-impl/bar (MyFooAImplementation. 'a 'b))))

And now for the other test case implementation:

(ns myfooBTests
    (:use [myfooprotocol-tests :only [testFoo testBar]])
    (:import [myfoo_b_impl MyFooAImplementation])
    (:use [abstractions])
    (:require [myfoo-b-impl])
    (:use [clojure.test]))

(deftest testForFoo []
    (is (testFoo myfoo-b-impl/foo (MyFooBImplementation. '1 '2))))

(deftest testForBar []
    (is (testBar myfoo-b-impl/bar (MyFooBImplementation. '1 '2))))

My 2 concrete test implementations (myFooATests and myFooBTests namespaces) look verbose, but all they are really doing is delegating the assertion logic to my 'testFoo' and 'testBar' functions in the myfooprotocol-tests namespace. It's just boilerplate code.

But there's a snag. In the last 2 listings, the first parameter to 'testFoo' and 'testBar' is 'myfoo-#-impl/foo' or 'myfoo-#-impl/bar' (where '#' is a or b). But this doesn't work because 'foo' and 'bar' functions are buried inside the defprotocol, and I cannot access them this way.

Because I'm pretty much learning Clojure in isolation, I wanted to reach out to the SO community and try to get some help. Firstly, is what I'm doing here in my Clojure code look remotely reasonable? I.e., this idea of trying to 'test to the interface (err, protocol) once' --- is this a worthy goal in the Clojure universe? (the DRY in me says so; as does the OO practitioner in me). If my interpretation of the relationship between protocols and records in Clojure is correct (i.e., a form of interface and implementation buddies), then I really only want to write my tests once (as I tried to do in the 'myfooprotocol-tests' namespace).

And secondly, assuming all of this is sane, how can I effectively pass the 'foo' and 'bar' functions that are defined inside the defrecords of the 'myfoo-a-impl' and 'myfoo-b-impl' namespaces? What is the syntax for getting at them?

Thank you for your time.

Upvotes: 2

Views: 536

Answers (1)

Ankur
Ankur

Reputation: 33657

First the easy part - Yes, your thinking to test various implementations of a protocol does make sense and it is useful.

Now the extremely easy part i.e how to go about it. Think of a protocol as creating a functions in a namespace (theoretically without implementation yet as that would happen when you extend that protocol). So when you say:

(ns myabstractions)

(defprotocol MyFooProtocol
    (foo [this] "returns some int")
    (bar [this] "returns some string"))

That means now myabstractions has 2 functions called foo and bar. As they are just functions we can refer them easily from this namespace i.e myabstractions/foo or myabstractions/bar. This makes it clear that you don't need to pass these functions to the generic test namespace functions, they just need a type (in your case a record) on which they can call foo or bar, hence:

(ns myfooprotocol-tests (:use [myabstractions]))

(defn testFoo [myFoo]
    (let [fooResult (foo myFoo)]
      (...some expression that returns a boolean...)))

(defn testBar [myBar]
    (let [barResult (bar myBar)]
      (...some expression that returns a boolean...)))

From your specific tests for each implementation you just need to pass the record instance that implements the protocol.

Upvotes: 4

Related Questions