模拟 Clojure 协议

发布于 2024-10-31 02:04:53 字数 209 浏览 3 评论 0 原文

可以使用流行的 Java 模拟框架之一,例如 EasyMockMockito 用于模拟使用 defprotocol 定义的 Clojure 协议?如果是这样,怎么办?

Can one of the popular Java mocking frameworks like EasyMock or Mockito be used to mock Clojure protocols defined with defprotocol? If so, how?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

旧时浪漫 2024-11-07 02:04:53

您应该能够使用任何模拟库来模拟协议。在幕后,每个协议都使用 Java 接口作为实现细节,您可以模拟该接口。

也就是说,不要这样做!由于反射、保护级别、最终类等原因,Java 中的模拟异常复杂。任何时候您想要一个实现协议的 Clojure 对象,只需调用 reify,例如 请

 (defprotocol Foo (method-a [_]) (method-b [_]))
 -> Foo

 (let [stub (reify Foo (method-a [_] :stubbed))] 
   (method-a stub))
 -> :stubbed

注意,您无需存根不打算调用的方法。

You should be able to mock protocols using any mock library. Under the covers, every protocol uses a Java interface as an implementation detail, and you could just mock that interface.

That said, don't do this! Mocking in Java is absurdly complex because of reflection, protection levels, final classes, etc. Any time you want a Clojure object that implements a protocol, simply call reify, e.g.

 (defprotocol Foo (method-a [_]) (method-b [_]))
 -> Foo

 (let [stub (reify Foo (method-a [_] :stubbed))] 
   (method-a stub))
 -> :stubbed

Note that you need not stub the methods you don't plan to call.

顾冷 2024-11-07 02:04:53

看起来最新版本的 Midje 很好地提供了此功能。

首先,我想指出,当将较大的程序拆分为组件时(例如,通过依赖注入与 Stuart Sierra 的 组件库)。如果我有一些组件将一组副作用函数隔离到一个概念组件中,我当然需要一个测试框架,它可以让我注入一个替代组件,这样我就可以:

  1. 编写在该组件之前使用该组件的代码存在(自上而下)。
  2. 测试使用该组件的功能,与该组件的实际实现隔离。

您可以使用 Mockito 或其他一些库,但我同意这样的解决方案不会特别优雅。

不幸的是,协议和记录生成 Midje 无法像函数一样轻松破解的类...因此您必须稍微修改代码:

(defrecord-openly SideEffectThing [connection]
 ISideEffect
 (persist [this thing] :unfinished)
 (read [this] :unfinished)
)

请参阅 Midje 关于生产模式的文档,详细了解如何使此修改不影响您的生产运行时。

通过使用 defrecord-openly 定义组件,您可以指定使用 Midje 的“提供”机制的组件方法的行为:

(fact "you can test in terms of a record's methods"
  (let [obj (->SideEffectThing :fake-connection)]
   (user-of-side-effect-thing obj) => 0
   (provided
    (read obj) => -1)
  )
 )

当然,您可以避免在这里依赖生产类型(我主张这样做),并且还可以避免在整个生产代码中公开地添加 defrecord。在这种情况下,只需将 SideEffectThing(如上所述)移至测试代码中即可。然后,应用程序中的组件系统可以插入真正的组件,但您的测试可以针对这个未实现的测试版本编写。

为了完整起见,我将把等效的 Java Mockito 代码与上述解决方案进行比较。在 Java 中:

interface ISideEffect { int read(); void write(Object something); }
class SideEffectThing implements ISideEffect { ... }

// in test sources:
public class SomeOtherComponentSpec {
   private ISideEffect obj;
   @Before
   public void setup() { obj = mock(ISideEffect.class); }
   @Test
   public void does_something_useful() {
      when(obj.read()).thenReturn(-1);
      OtherComponent comp = new OtherComponent(obj);

      int actual = comp.doSomethingUseful();

      assertEquals(0, actual);
      verify(obj).read();
   }

此 Java 解决方案模拟组件,指定该组件所需的行为,然后不仅检查组件本身是否正常工作,而且还检查组件以某种方式依赖于对 read() 的调用。 Mockito 还支持参数(和捕获)的模式匹配,以分析组件的使用方式(如果需要)。

上面的 Midje 示例完成了大部分工作,并且形式更加清晰。如果您(在提供的子句中)指示使用特定参数调用该函数,则如果不是,则测试将失败。如果您指定该函数被多次调用(但事实并非如此),那么就会失败。例如,指示 read 将被调用 3 次并且应该返回不同的值:

(fact "you can test in terms of a record's methods"
  (let [obj (->SideEffectThing :fake-connection)]
   (user-of-side-effect-thing obj) => 0
   (provided
    (read obj) => -1
    (read obj) => 6
    (read obj) => 99
    )
  )
 )

表示您希望 read 被调用 3 次,并且它应该返回指定的值序列。请参阅有关先决条件的文档了解更多详细信息,包括如何指定提供的单个函数规范的确切次数,以及如何指示该函数从未被调用。

It looks like the more recent versions of Midje provide this functionality quite nicely.

First, I would like to note that this kind of mocking is very commonly useful when splitting larger programs into components (e.g. that are assembled via dependency injection with libraries like Stuart Sierra's component library). If I've got some component that isolates a set of side-effect functions into a conceptual component I certainly want a testing framework that will let me inject a stand-in component so that I can:

  1. Write code that uses the component before it even exists (top-down).
  2. Test functions that use that component in isolation from the real implementation of the component.

You could use Mockito or some other library, but I agree such a solution is not going to be particularly elegant.

Unfortunately, protocols and records generate classes that Midje cannot hack into as easily as functions...so you do have to modify your code slightly:

(defrecord-openly SideEffectThing [connection]
 ISideEffect
 (persist [this thing] :unfinished)
 (read [this] :unfinished)
)

See Midje's documentation on production mode for details about how to make this modification not affect your production runtime.

By defining your component with defrecord-openly, you gain the ability to specify the behavior of the component's methods using Midje's "provided" mechanism:

(fact "you can test in terms of a record's methods"
  (let [obj (->SideEffectThing :fake-connection)]
   (user-of-side-effect-thing obj) => 0
   (provided
    (read obj) => -1)
  )
 )

You could, of course, avoid relying on a production type here (which I would advocate), and also avoid peppering defrecord-openly throughout your production code. In this case, just move SideEffectThing (as written above) into your test code. Then your component system in the application can plug in the real component, but your tests can be written against this unimplemented test version.

To be complete, I will compare the equivalent Java Mockito code with the above solution. In Java:

interface ISideEffect { int read(); void write(Object something); }
class SideEffectThing implements ISideEffect { ... }

// in test sources:
public class SomeOtherComponentSpec {
   private ISideEffect obj;
   @Before
   public void setup() { obj = mock(ISideEffect.class); }
   @Test
   public void does_something_useful() {
      when(obj.read()).thenReturn(-1);
      OtherComponent comp = new OtherComponent(obj);

      int actual = comp.doSomethingUseful();

      assertEquals(0, actual);
      verify(obj).read();
   }

this Java solution mocks out the component, specifies required behavior of that component, and then not only checks that the component itself works properly, but also that the component depended on the call to read() in some way. Mockito also supports pattern matching on arguments (and capture) to analyze how the component was used, if needed.

The Midje example above does much of that, and in a much clearer form. If you indicate (in the provided clause) that the function is called with specific arguments, then the test will fail if it isn't. If you specify that the function is called more than once (and it isn't), then that is a failure. For example, to indicate the read will be called 3 times and should return different values:

(fact "you can test in terms of a record's methods"
  (let [obj (->SideEffectThing :fake-connection)]
   (user-of-side-effect-thing obj) => 0
   (provided
    (read obj) => -1
    (read obj) => 6
    (read obj) => 99
    )
  )
 )

indicates that you expect read to be called three times, and that it should return the specified sequence of values. See the docs on prerequisites for more details, including how to specify exact number of times on a single function spec in provided, and how to indicate the function is never called.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文