如何在 Lua 中使用反射(在运行时)?

How can I use reflection (at runtime) in Lua?

我正在尝试在 Lua 中使用 Busted 对代码进行单元测试(我没有写,也不允许我此时出于业务原因重构),并且这个模块中没有概念类 也不是依赖注入。所以,我想用我制作的模拟记录器替换文件顶部所需的一些模块,即 local log = require("path.to.module.logger"):new() 来跟踪方法被调用的次数,即 logger:trace() 这样的与 Java 中 Mockito 中的 times() 一样。在 Java 中,我可以使用 Reflection.Utils 来达到这个目的。 Lua 中有什么等价物可以帮助使这个不可测试的代码可测试?

我已经尝试创建一个具有相同变量名称的全局变量 log 并使用此示例将其设置为等于我的模拟:https://www.lua.org/pil/14.2.html

本地 _M = {}

本地日志=要求("path.to.module.logger"):new()

...

函数_M.init(...) log:trace("debug") # 我希望这个日志实例不是上面那个,而是我在运行时注入模块的那个 结束

"Reflection" 在 Lua 中并不是真正的东西,在 Java 这个词的意义上不是。作为一门使用Duck Typing的语言,一切都很开放。 Lua只有一种数据结构:atable。 Lua 中的所有内容 来自 table。模块只是 table return 由 require.

加载的块编辑

table后面的内容和数据结构可以通过metatable隐藏起来,可以用来阻止正常的迭代过程(pairs,ipairs,等等)用于访问 table 中的元素。但是,您始终可以使用 getmetatable 提取元 table 本身并调用它;你甚至可以突破用 debug.getmetatable.

隐藏 metatable 的常用方法

话虽这么说,因为 Lua 依赖于 Duck Typing,并且因为 Lua 的 API 非常开放,所以很难 全面 关于包装每个函数和 table。

例如,假设您要包装一个模块。这很容易;只需创建一个空的 table,它有一个 metatable,其元方法调用包装的模块方法。这适用于模块的直接 APIs。

但是,如果其中一个 API return 是一个 自身 需要包装的对象,会发生什么?您将如何区分专门的 API 对象和普通的 table 对象?同样重要的是,如果您能够成功识别出哪些 return 值需要包装,您将如何做到这一点?毕竟,如果他们将你的包装器 table 之一传递给包装器 API 函数,它现在需要解包 table 以便它可以将包装的 table 传递给被包装的实际功能。

今天早上我实际上能够从一位同事那里找到答案。 "Reflection" 并不是真的可能,正如尼可波拉斯在他的回答中建议的那样,然而,从 lua 文档 (http://lua-users.org/wiki/ModulesTutorial) 中,我们了解到:

Lua caches modules in the package.loaded table.

这意味着我们可以在失败的测试中覆盖 package.loaded table 并且基本上在运行时替换紧耦合代码中的依赖项(很像通过 [=25 中的依赖注入在 Mockito 中进行模拟) =]).例如:

package.loaded["path.to.module.logger"] = my_logger 将在全球范围内用 my_logger 替换依赖项 path.to.module.logger 假设它遵守相同的合同。

我会为记录器编写模拟并将其设置在与原始记录器相同的路径下,但在不同的根目录下。然后为测试在 LUA_PATH

的开头添加带有模拟的文件夹

示例: /tmp/a/package/logger.lua :

local _M = {}

_M.log = function()
  print "Original logger"
end

return _M

/tmp/b/package/logger.lua:

local _M = {}

_M.log = function()
  print "Mocked logger"
end

return _M

和测试/tmp/test/logger_spec.lua:

describe("Test suite", function()
  it("Testing the mock", function ()

    local log = require("package.logger")

    log.log()
  end)
end)

如果您将 LUA_PATH 设置为使用原始 : export LUA_PATH="/tmp/a/?.lua;;" 并拨打电话:

busted logger_spec.lua
Original logger
●
1 success / 0 failures / 0 errors / 0 pending : 0.000527 seconds

现在将 LUA_PATH 指向您的模拟: export LUA_PATH="/tmp/b/?.lua;;"

电话又被打断了

busted logger_spec.lua
Mocked logger
●
1 success / 0 failures / 0 errors / 0 pending : 0.000519 seconds