【问题标题】:Is there a mocking/stubbing framework for Common Lisp?Common Lisp 是否有模拟/存根框架?
【发布时间】:2010-11-01 11:11:34
【问题描述】:

是否有用于 Common Lisp 的模拟/存根框架?

EmacsLispMock 看起来不错,但它是一个 Emacs lisp 框架,我正在寻找可以从 Common Lisp 中使用的东西。

有什么建议吗?

【问题讨论】:

  • 模拟/存根框架有什么作用?
  • @Xach 这个想法是让您通过控制其他函数的行为来单独测试给定函数。因此,如果您有一个调用函数 B 的函数 A,您可以存根 B 以始终返回 5 或其他值,并验证 A 是否按照该返回值执行了它应该执行的操作。这样您就可以验证 A 是否工作,而无需调用实际的 B。一个常见的场景是测试依赖于数据库访问的代码,而无需为每个测试设置和配置数据库。
  • 如果我想要的话,我可能只是将“存根函数 B”定义为 (defun b (&rest args) 5),具体来说,有一个返回 5 的函数。一旦到位并使用我的函数“B”已经过测试,重新加载正确的定义。
  • @vatine 如果你想在 B 之前实现 A,这在开发过程中会很有用,但是如果你用正确的实现重新定义 B,你将失去对 A 执行独立回归测试的能力。例如,如果 A 应该能够处理来自 B 的整数和 nil,我希望对这两种情况进行回归测试,这些情况与 B 的实际实现是隔离的(因为如果它依赖于文件系统,它可能会很慢且难以控制,数据库等)。
  • 如果你做类似 (defvar b-return-value) 的事情,那么将 b 定义为 (defun b (&rest args) b-return-value i>)),你可以包装一个 LET,重新绑定 b-return-value。不过,这仍然意味着您需要有一个用于测试的定义和一个用于实际操作的定义。

标签: unit-testing tdd mocking lisp common-lisp


【解决方案1】:

以下应该可以满足您的需求

(defmacro with-replaced-function (fdef &rest body)
  (let ((oldf (gensym))
        (result (gensym))
        (name (car fdef))
        (args (cadr fdef))
        (rbody (cddr fdef)))
    `(let ((,oldf (symbol-function ',name)))
       (setf (symbol-function ',name) (lambda ,args ,@rbody))
       (let ((,result (progn ,@body)))
         (setf (symbol-function ',name) ,oldf)
         ,result))))

(defmacro show (x)
  `(format t "~a --> ~a~%"
           ',x ,x))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun foo (x y) (+ x y))

(defun bar (x) (foo x (* x 2)))

(show (bar 42))

(show (with-replaced-function (foo (x y) (* x y))
                              (bar 42)))

(show (bar 42))

宏只是保存符号当前指向的函数,并用提供的存根实现替换它。在块结束时,函数恢复为原始值。

添加保护以防止身体的非本地退出可能是有意义的。

还要注意,如果函数调用已被编译器内联,那么显然更改函数定义将不起作用。 CL 有一个特殊的NOTINLINE 声明可以用来防止这个问题。

【讨论】:

  • 应该注意,这可能不一定在编译后的代码中起作用,因为文件编译器可能会内联函数(例如,编译器可以假设同一文件中的函数保持不变)。所以可能有必要声明这些函数不被内联。
  • 使用unwind-protect 完成对非本地退出的保护。这是一个非常基本的模式,经常出现在宏中。
【解决方案2】:

正如 Rainer 指出的,文件编译器有时会内联函数,这意味着更改函数定义不会对内联函数的地方产生任何影响。

更改函数名称的定义也不会取代将函数用作文字对象,例如,如果您将 #'my-stubbed-function 保存在某个变量中。

但是,在某些 lisp 实现中,您可以定义函数包装器或使用建议来实现此目的:例如,Allegro CL 有 fwrappers。在 SBCL 中,您可以将 TRACE 与非标准 :break 函数和自定义 *invoke-debugger-hook* 一起使用,我相信其他 lisp 实现也会有类似的东西。

我认为没有人将这些存根方法打包到库中。你能成为第一个! (它会很棒。我很想用这样的东西。)

【讨论】:

    【解决方案3】:

    这不是最简单的方法吗?

    > (defun b () 'original)
    B
    > (setf f #'b)
    #<Compiled-function B #xC2C1546>
    > (defun a () (funcall f))
    A
    > (a)
    ORIGINAL
    > (setf f #'(lambda () 'stub))
    #<Anonymous Function #xC2D990E>
    > (a)
    STUB
    > (setf f #'b)
    #<Compiled-function B #xC2C1546>
    > (a)
    ORIGINAL
    

    【讨论】:

    • 有趣的建议。因此,这意味着您必须通过使用 funcall 而不是普通调用来编写具有某些可替换的函数调用的代码。也许这是常见的 lisp 方式? IE。要使用的实现在运行时可能会有所不同,因此请使用 funcall。
    【解决方案4】:

    我编写了一个库,其宏与@6502 的答案 (with-mocked-functions) 非常相似,但更通用一些。它还提供了with-added-methods,它允许您为有限的动态范围编写模拟方法。你可以在这里找到它:https://github.com/bytecurry/bytecurry.mocks

    【讨论】:

      【解决方案5】:

      您不需要 CL 中的模拟/存根框架。

      只需使用 ovverides 方法创建从您的类类派生的新 CLOS 以用于您想要存根/模拟的内容,然后您就完成了。

      至于存根,为什么不重新定义函数呢?

      【讨论】:

      • 感谢您的回答。我也在寻找一种适用于不使用 CLOS 的代码的解决方案。我希望能够对调用另一个函数的任何函数进行存根。
      • 我相信 FLET 只将函数绑定到指定词汇上下文中的符号。
      • 如果以后有一种“恢复”定义的好方法,重新定义函数会起作用,因为我希望能够在不弄乱实际函数定义的情况下运行测试。有什么想法吗?
      【解决方案6】:

      几年后,有。我们在 Quicklisp 中有 cl-mockmockingbird

      (ql:quickload :mockingbird)
      

      这还允许检查一个函数是否被调用,如果调用了多少次以及使用哪些参数,并且可以存根单个方法。

      【讨论】:

        【解决方案7】:

        您可以尝试将函数重新定义包装在宏中

        (defmacro with-fun (origfn mockfn &body body)
          `(let ((it ,origfn))
              (setf ,origfn ,mockfn)
             ,@body
              (setf ,origfn ,it)))
        

        这只是一个想法,你必须实现这样的宏。 您可以搜索 profile 实现,它正是这样做的,用另一个函数替换一个函数并添加分析信息。你可以从那里借鉴一些想法。

        【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-05-10
        • 1970-01-01
        • 1970-01-01
        • 2010-10-07
        • 1970-01-01
        • 2012-11-06
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多