在 Java 世界中,当谈到开发单元测试时,我遵循了“测试接口”的方法。这意味着,如果我有一个 Java 接口,我将为该接口编写一个单元测试类(从 JUnit 的 TestCase 扩展或其他);测试该接口。这个类将是抽象的,并且将包含一系列用于测试我的接口方法的测试方法。这是一个简单的例子:
/** 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();
}
}
所以在这里我们有一件很棒的事情。无论我们有多少MyFooInterface 的实现,我们只需要编写一组单元测试(在TestMyFooInterface.java 中)来保证MyFooInterface 的契约正确性。然后,我们只需要为我们拥有的每个接口实现一个具体的测试用例。这些具体的测试用例很无聊;他们需要做的就是提供“getMyFooInterface”的实现;他们只是通过构建正确的实现类来做到这一点。现在,当我运行这些测试时,将为每个具体的测试类调用 TestMyFooInterface 中的每个测试方法。顺便说一句,当我说“当我运行这些测试时”时,这意味着将创建一个 TestMyFooImplA 的实例(因为它是测试工具找到的具体测试用例;基于 Ant 或基于 Maven 或其他的东西)及其所有“测试”方法将运行(即,来自 TestMyFooInterface 的所有方法)。TestMyFooImplB 也将被实例化,并且它的“测试”方法将被运行。砰!我们只需要编写一组测试方法,它们将为我们创建的每个具体测试用例实现运行(只需要几行代码!)
好吧,当涉及到协议和记录时,我想在 Clojure 中反映同样的方法,但我偶然发现了一点。另外,我想验证这种方法在 Clojure 世界中是否合理。
到目前为止,这是我在 Clojure 中的内容。这是我的“界面”:
(ns myabstractions)
(defprotocol MyFooProtocol
(foo [this] "returns some int")
(bar [this] "returns some string"))
现在我可能有这个协议的 2 个不同的实现,以记录的形式。这是一个实现:
(ns myfoo-a-impl
(:use [myabstractions]))
(defrecord MyFooAImplementation [field-a field-b]
MyFooProtocol
(foo [this] ...impl here...)
(bar [this] ...impl here...))
还有另一种实现:
(ns myfoo-b-impl
(:use [myabstractions]))
(defrecord MyFooBImplementation [field-1 field-2]
MyFooProtocol
(foo [this] ...impl here...)
(bar [this] ...impl here...))
所以在这一点上,我在我熟悉的 OO Java 世界中处于同样的位置。我的 MyFooProtocol 协议有 2 个实现。每个实现的 'foo' 和 'bar' 函数都应该遵守 MyFooProtocol 中记录的函数的约定。
在我看来,我只想为“foo”和“bar”创建一组测试,即使我有多个实现,就像我在 Java 示例中所做的那样。这就是我接下来对 Clojure 代码所做的事情。我创建了我的测试:
(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...)))
太好了,我只写过一次测试。上面的每个函数都返回一个布尔值,有效地表示一些测试用例/断言。实际上,我还有很多很多(对于我想做的每个断言)。现在,我需要创建我的“实现”测试用例。好吧,由于 Clojure 不是面向对象的,所以我不能真正做我在上面的 Java 示例中所做的事情,所以这就是我的想法:
(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))))
现在对于其他测试用例实现:
(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))))
我的 2 个具体测试实现(myFooATests 和 myFooBTests 命名空间)看起来很冗长,但他们真正做的只是将断言逻辑委托给 myfooprotocol-tests 命名空间中的“testFoo”和“testBar”函数。这只是样板代码。
但有一个障碍。在最后 2 个清单中,“testFoo”和“testBar”的第一个参数是“myfoo-#-impl/foo”或“myfoo-#-impl/bar”(其中“#”是 a 或 b)。但这不起作用,因为 'foo' 和 'bar' 函数被隐藏在 defprotocol 中,我无法以这种方式访问它们。
因为我几乎是孤立地学习 Clojure,所以我想联系 SO 社区并尝试获得一些帮助。首先,我在 Clojure 代码中所做的事情看起来是否合理?即,这种尝试“对接口(错误,协议)进行一次测试”的想法——这在 Clojure 世界中是一个有价值的目标吗?(我的 DRY 是这么说的;我的 OO 从业者也是如此)。如果我对 Clojure 中协议和记录之间关系的解释是正确的(即接口和实现伙伴的一种形式),那么我真的只想编写一次测试(就像我尝试在 'myfooprotocol-tests' 命名空间中所做的那样)。
其次,假设所有这些都是正常的,我如何有效地传递在“myfoo-a-impl”和“myfoo-b-impl”命名空间的 defrecords 中定义的“foo”和“bar”函数?获取它们的语法是什么?
感谢您的时间。