两者看起来都相当不错。我想了解每个库特别擅长或缺乏什么,特别是对于测试 Web 应用程序。
5 回答
我没用过speclj,我是Midje的第一作者。其他人没有提到的一点是,Midje 试图利用函数式语言和面向对象语言之间的差异。
一个区别是不变性。因为大多数函数只依赖于它们的输入,而不依赖于包含的状态,所以你对它们所做的真实陈述在感觉上与面向对象的对应物不同。在 OO 测试中,您可以制作以下形式的示例:“鉴于此历史记录和这些输入,此方法会产生某某。”
函数式语言中的示例似乎只是更简单的示例:“给定这些输入,此函数返回某某”。但我认为这不太对。我认为系统中的其他功能起着类似于状态/历史的作用:它们是您试图通过智力控制的事情之一。函数及其关系是您希望测试帮助您清晰思考的内容。
出于这个原因,Midje 的编写假设一个甜蜜的开发过程涉及到:
- 我想对这个功能做什么?在系统的世界里,有什么好的方法来思考这个函数的作用?
- 在这样做的过程中,还有哪些其他功能有用——将捕获领域的重要部分——以及我想对它们做出什么样的真实陈述?
然后,在典型的 mockist 风格中,你大致自上而下或由外而内地开发,当你从错误中恢复或有更好的想法时,允许不可避免的迭代。
最终结果是一大堆函数,它们的相互关系由测试记录,或者(正如 Midje 所说的)关于函数和它们所依赖的函数的“事实”。许多人评论说,Midje 有一种 Prolog/逻辑编程的感觉,这不是偶然的。与往常一样,测试只是示例,但 Midje 试图让它们读起来更像是事实陈述。这就是它唯一真正创新的特性元常数的理由。这是他们的一个例子:
(fact "right changes the direction, but not the position"
(right (snapshot north ...position...)) => (snapshot west ...position...)
(right (snapshot east ...position...)) => (snapshot north ...position...)
(right (snapshot south ...position...)) => (snapshot east ...position...)
(right (snapshot west ...position...)) => (snapshot south ...position...))
在这种情况下,实际位置与 function 的真实情况无关right
,只是它永远不会改变。元常数的想法是,它是一个除了测试中明确说明的值外,一无所知的值。在测试中,很难区分什么是必要的,什么是偶然的。这有许多不好的影响:理解、可维护性等。元常量提供了清晰性。如果值是包含 key 值的映射或记录很3
重要:a
,您可以明确地说:
(fact
(full-name ..person..) => "Brian Marick"
(provided
..person.. =contains=> {:given-name "Brian", :family-name "Marick"}))
这个测试明确了关于人的事情——也明确了不重要的事情(除了两个名字之外的任何东西)。
在数学术语中,Midje 试图让您做出诸如“for all x where x...”之类的陈述,同时仍然是一个测试工具而不是定理证明者。
这种方法的灵感来自于《 Growing Object-Oriented Software》中描述的那种“伦敦风格”的模拟重度 TDD ,这是我在编写 Ruby 代码时通常使用的方法。但事实证明它有一种完全不同的感觉,以一种难以描述的方式。但感觉需要更多的工具支持而不仅仅是with-redefs
.
结果是,Midje 在某种程度上是在尝试寻找一种功能性 TDD 风格,而不仅仅是 OO TDD 的移植。它也试图成为一个通用工具,但它是半固执的软件。正如亚伯拉罕·林肯所说:“那些喜欢这种东西的人会发现这是他们喜欢的东西。”
我肯定会选择Speclj。
Speclj 易于集成和使用。它的语法没有 Midje 的那么华丽。Speclj 基于 RSpec,为您提供 Ruby 程序员习惯的所有舒适性,同时又不会失去 Clojure 的特质。
Speclj 中的自动跑步者很棒。
lein spec -a
一旦你使用了一段时间,你会想知道当你不得不手动运行测试时你是如何完成工作的。
模拟不是问题,因为您可以简单地使用 with-redefs。@rplevy 在 Speclj 中的示例看起来像这样。
(ns foo.core-spec
(:require [speclj.core :refer :all ]
[foo.core :as base]))
(describe "Core"
(it "contrives 100"
(let [message-params (atom nil)]
(with-redefs [base/timestamp (fn [] 1350526304739)
base/important-message #(reset! message-params [%1 %2])]
(should= {:x 100 :timestamp 1350526304739} (base/contrived 100))
(should= 100 (first @message-params))))))
这种简单的模拟方法是中肯的。没有误导。
至于测试网络应用程序,Speclj 工作正常。事实上,Speclj 支持内置于Joodo中。
免责声明:我写了 Speclj
使用 Midje 的最大好处是它提供了用于测试事物的集中抽象,而无需测试它们的所有部分,而这些部分通常会拖累整个世界。
如果您的函数涉及调用辅助函数以生成时间戳、将某些内容放入数据库或消息队列、发出 API 请求、缓存某些内容、记录某些内容等,您想知道这些涉及世界的函数调用发生了(有时它们发生了多少次),但是实际执行它们与您正在测试的函数无关,并且被调用的函数通常应该有自己的单元测试。
假设你的代码中有这个:
(defn timestamp [] (System/currentTimeMillis))
(defn important-message [x y] (log/warnf "Really important message about %s." x))
(defn contrived [x & y]
(important-message x y)
{:x x :timestamp (timestamp)})
以下是使用 midje 进行测试的方法:
(ns foo.core-test
(:require [midje.sweet :refer :all]
[foo.core :as base]))
(fact
(base/contrived 100) => {:x 100 :timestamp 1350526304739}
(provided (base/timestamp) => 1350526304739
(base/important-message 100 irrelevant) => anything :times 1))
这个例子只是对 midje 可以做什么的快速一瞥,但展示了它擅长的本质。在这里,您可以看到需要表达的无关复杂性非常少:
- 函数应该产生什么(尽管每次调用函数时时间戳函数会产生不同的结果),
- 调用了时间戳函数和日志记录函数,
- 日志记录函数只被调用一次,
- 日志记录函数收到了预期的第一个参数,并且
- 你不在乎它收到的第二个参数是什么。
我试图用这个例子说明的要点是,它是一种非常简洁和紧凑的方式,可以用简单的部分来表达复杂代码的测试(我所说的复杂是指它具有可以分离的嵌入部分),而不是尝试测试所有内容一次全部。一次测试所有东西都有它的位置,即在集成测试中。
我承认有偏见,因为我积极使用 midje,而我只看过 speclj,但我的感觉是,speclj 可能对使用过类似 Ruby 库并发现这种思考测试理想方式的人最有吸引力,基于经验。这是选择测试框架的一个完全合理的理由,并且可能还有其他关于它的好东西,希望其他人可以评论。
我想说 Midje 特别擅长创建用于表达存根和模拟的 DSL。如果您关心存根和模拟,并且想经常使用它,我会选择 Midje 而不是 Speclj,因为它具有用于表达那些比 slagyr 在他的答案中提供的方法更简洁的测试类型的抽象。
如果您想要更轻量级的方法,另一种选择是打算与 clojure.test 一起使用的Conjure存根/模拟库。
Speclj 的亮点在于它很像 RSpec,包含“描述”和“它”……Midje 实际上可以支持嵌套事实,但不如 Speclj 优雅。
免责声明:我是 Midje 和 Conjure 的贡献者。:)
我会建议 Midje 而不是 Speclj
对于 speclj,我不认为它是否对模拟有很好的支持,与 Midje 相比,文档看起来也很稀疏。
Midje 的语法也更好:
(foo :bar) => :result compared to (should= (foo :bar) :result)