2

在 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”函数?获取它们的语法是什么?

感谢您的时间。

4

1 回答 1

4

首先是简单的部分 - 是的,您测试协议的各种实现的想法确实有意义并且很有用。

现在是极其简单的部分,即如何去做。将协议视为在命名空间中创建函数(理论上尚未实现,因为当您扩展该协议时会发生这种情况)。所以当你说:

(ns myabstractions)

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

这意味着现在 myabstractions 有 2 个函数,称为 foo 和 bar。由于它们只是函数,我们可以从这个命名空间中轻松引用它们,即myabstractions/foomyabstractions/bar. 这清楚地表明您不需要将这些函数传递给通用测试命名空间函数,它们只需要一个可以调用 foo 或 bar 的类型(在您的情况下为记录),因此:

(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...)))

从您对每个实现的特定测试中,您只需要传递实现协议的记录实例。

于 2013-05-16T04:56:14.040 回答