Zenn
🌒

OpenResty + Luaで単体テストを書く際にハマったこと

2025/03/23に公開

これは何?

OpenResty環境で使用しているLua Scriptのシステムで単体テストを書きたいと思ったのですが,そこそこ詰まる部分があったので詰まった部分をまとめました。


環境

  • OpenResty 1.21.4.1
  • busted: Lua Scriptの単体テスト用フレームワーク

詳しくは自分のリポジトリ参照。


ハマりポイント①LuaJITとLuaのバージョンをあっていない際にライブラリがうまく動かないことがある

事象: bustedがうまく実行できない

/usr/local/openresty/luajit/bin/luajit: /usr/local/openresty/lua54/bin/busted:3: unexpected symbol near '-'

前提: LuaJITとは

LuaJIT is a Just-In-Time Compiler for the Lua programming language.
Homepage: http://luajit.org/luajit.html
LuaJIT is enabled by default since OpenResty 1.5.8.1. Please explicitly specify the --with-luajit option while configuring OpenResty older than 1.5.8.1. See Installation for details.
OpenResty公式ドキュメントLuaJIT

OpenRestyは1.5.8.1以降はLuaJITを使用しています。
LuaJITを使うことで通常のLuaよりも高速に動作します。

解決策: LuaRocksでベースとするLuaのバージョンをLuaJITにあわせる

  • LuaRocksはLuaで使用するライブラリの管理ツール
  • LuaRocksのインストールにはHereRocksを使うとLuaやLuaJITとセットでインストールできる

自分はもともと,Lua5.4を使用していましたが,LuaJITの公式ドキュメントによるとLuaJITはLua5.1をベースに作成されています。

このため,LuaRocksをLua5.1を使用するように変更しました。
以下はluajit 2.1.0-beta3にあわせてluarocksをインストールする例です

usr/local/openresty/luajit/bin/luajit -v
LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2022 Mike Pall. https://luajit.org/
hererocks luajit21 -j 2.1.0-beta3 -r latest

ライブラリによっては最新のLua5.4にあわせてインストールしても動くものもありそうだが,無難にLuaJITに合わせるべきと思われる。


ハマりポイント②LUA_PATHのexportが必要

事象: module not foundエラーがでる

以下は自分のbustedの実行例ですが,LUA_PATHが通っていないと.soを探して見つからないエラーがでます。

<details><summary>エラー分の詳細</summary>

/usr/local/openresty/luajit21/bin/busted -p _test tests

Error → tests/auth_factory_test.lua @ 1
auth_factory.lua get_auth_instance
./src/auth_factory.lua:2: module 'basic_auth' not found:No LuaRocks module found for basic_auth
        no field package.preload['basic_auth']
        no file './src/basic_auth.lua'
        no file './src/basic_auth/basic_auth.lua'
        no file './src/basic_auth/init.lua'
        no file '/usr/local/openresty/luajit21/share/lua/5.1/basic_auth.lua'
        no file '/usr/local/openresty/luajit21/share/lua/5.1/basic_auth/init.lua'
        no file '/root/.luarocks/share/lua/5.1/basic_auth.lua'
        no file '/root/.luarocks/share/lua/5.1/basic_auth/init.lua'
        no file './csrc/basic_auth.so'
        no file './csrc/basic_auth/basic_auth.so'
        no file '/usr/local/openresty/luajit21/lib/lua/5.1/basic_auth.so'
        no file './basic_auth.so'
        no file '/usr/local/openresty/luajit21/lib/lua/5.1/loadall.so'
        no file '/root/.luarocks/lib/lua/5.1/basic_auth.so'
/usr/local/openresty/reverse_proxy/tests

</details>

解決策: LUA_PATHのexport

LUA_PATHの内容はnginx.confに記載のあるlua_package_pathをコピペしました。TODO: 必要最低限を探してもいいかも

export LUA_PATH="/usr/local/openresty/lualib/?.lua;/usr/local/openresty/luajit/libs/?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/jit/?.lua;/usr/local/openresty/reverse_proxy/src/?.lua;/usr/local/openresty/reverse_proxy/src/auth/?.lua;/usr/local/openresty/lualib/resty/?.lua;/usr/local/openresty/lualib/ngx/?.lua;/usr/local/openresty/?.lua;/usr/local/openresty/luajit21/share/lua/5.1/?.lua"

ハマりポイント③busted実行時にLuaJITに依存するライブラリが使えない

事象: ffi not foundエラーがでる

/usr/local/openresty/lua54/share/lua/5.4/resty/md5.lua:4: module 'ffi' not found:
        No LuaRocks module found for ffi
        no field package.preload['ffi']
        no file './src/ffi.lua'
        no file './src/ffi/ffi.lua'
        no file './src/ffi/init.lua'
        no file '/usr/local/openresty/lua54/share/lua/5.4/ffi.lua'
        no file '/usr/local/openresty/lua54/share/lua/5.4/ffi/init.lua'
        no file '/usr/local/openresty/lualib/ffi.lua'
        no file '/usr/local/openresty/luajit/libs/ffi.lua'
        no file '/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/jit/ffi.lua'
        no file '/usr/local/openresty/reverse_proxy/src/ffi.lua'
        no file '/usr/local/openresty/reverse_proxy/src/auth/ffi.lua'
        no file '/usr/local/openresty/lualib/resty/ffi.lua'
        no file '/usr/local/openresty/lualib/ngx/ffi.lua'
        no file '/usr/local/openresty/ffi.lua'
        no file '/root/.luarocks/share/lua/5.4/ffi.lua'
        no file '/root/.luarocks/share/lua/5.4/ffi/init.lua'
        no file './csrc/ffi.so'
        no file './csrc/ffi/ffi.so'
        no file '/usr/local/openresty/lua54/lib/lua/5.4/ffi.so'
        no file '/usr/local/openresty/lua54/lib/lua/5.4/loadall.so'
        no file './ffi.so'
        no file '/root/.luarocks/lib/lua/5.4/ffi.so'

解決策: ffiを使用できるようにbustedの実行コマンドを変更する

一部のライブラリはLuaJitでのみサポートされているffiに依存します。
調査したところ,以下のようにfflをインポートすれば解決できるようです。

https://github.com/lunarmodules/busted/issues/369

以下は実行例です。

local ffi = require "ffi"
export LUA_PATH="/usr/local/openresty/lualib/?.lua;/usr/local/openresty/luajit/libs/?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/jit/?.lua;/usr/local/openresty/reverse_proxy/src/?.lua;/usr/local/openresty/reverse_proxy/src/auth/?.lua;/usr/local/openresty/lualib/resty/?.lua;/usr/local/openresty/lualib/ngx/?.lua;/usr/local/openresty/?.lua;/usr/local/openresty/luajit21/share/lua/5.1/?.lua"

pushd ../
    # NOTE: ffiはluajitでのみしかサポートされていないため,https://github.com/lunarmodules/busted/issues/369 を参考に修正
    /usr/local/openresty/luajit21/bin/busted --helper=/usr/local/openresty/reverse_proxy/tests/helper.lua -p _test tests
popd


ハマりポイント④OpenRestyが絡む部分でngx変数周りのnilに対処が必要

事象: nilエラーがでている

以下は実行例です。

/usr/local/openresty/reverse_proxy /usr/local/openresty/reverse_proxy/tests
✱
0 successes / 0 failures / 1 error / 0 pending : 0.001674 seconds

Error → tests/auth_factory_test.lua @ 1
auth_factory.lua get_auth_instance
...ocal/openresty/luajit21/share/lua/5.1/resty/template.lua:147: attempt to index field 'location' (a nil value)
/usr/local/openresty/reverse_proxy/tests

解決策: ngxをMockする

有志が作っていたfakengxというツールもあるようですが,現在はPublic Archiveになっているので,必要な変数のみを自前でモックすることにしました。

https://github.com/bsm/fakengx

GitHub Copilot Chat/Edit等にエラー文をいれると勝手にMock内容を増やしてくれるで,いい時代になりましたね。

describe("auth_factory.lua get_auth_instance", function()
    -- NOTE: ngxのモックが必要だったので雑に作成。ngxをモックする公式のライブラリはなさそう
    _G.ngx = {
        var = {},
        log = function() end,
        ERR = "ERR",
        INFO = "INFO",
        HTTP_INTERNAL_SERVER_ERROR = 500,
        exit = function() end,
        re = {match = function() return nil end},
        location = {},
        config = {prefix = function()
            return "/usr/local/openresty/nginx/"
        end},
        get_phase = function() return "init" end,
        socket = {
            tcp = function()
                return {
                    settimeout = function() end,
                    connect = function() return true end,
                    setkeepalive = function() end,
                    close = function() end,
                    send = function() return true end,
                    receive = function() return nil end
                }
            end
        }
    }

最後に: サンプル

被テストコード

パラメータによって必要な認証のインスタンスを返す関数です。

local _M = {}
local basic_auth = require "basic_auth"
local digest_auth = require "digest_auth"
local form_auth = require "form_auth"

local _M = {}

function _M.get_auth_instance(auth_type)
    if auth_type == "basic" then
        return require "basic_auth"
    elseif auth_type == "digest" then
        return require "digest_auth"
    elseif auth_type == "form" then
        return require "form_auth"
    else
        error("Invalid authentication type: " .. auth_type)
    end
end

return _M

テストコード

describe("auth_factory.lua get_auth_instance", function()
    -- NOTE: ngxのモックが必要だったので雑に作成。ngxをモックする公式のライブラリはなさそう
    _G.ngx = {
        var = {},
        log = function() end,
        ERR = "ERR",
        INFO = "INFO",
        HTTP_INTERNAL_SERVER_ERROR = 500,
        exit = function() end,
        re = {match = function() return nil end},
        location = {},
        config = {prefix = function()
            return "/usr/local/openresty/nginx/"
        end},
        get_phase = function() return "init" end,
        socket = {
            tcp = function()
                return {
                    settimeout = function() end,
                    connect = function() return true end,
                    setkeepalive = function() end,
                    close = function() end,
                    send = function() return true end,
                    receive = function() return nil end
                }
            end
        }
    }

    local auth_factory = require "auth_factory"

    it("should return basic auth instance", function()
        local auth_instance = auth_factory.get_auth_instance("basic")
        assert.is.equal(auth_instance, require "basic_auth")
    end)

    it("should return digest auth instance", function()
        local auth_instance = auth_factory.get_auth_instance("digest")
        assert.is.equal(auth_instance, require "digest_auth")
    end)

    it("should return form auth instance", function()
        local auth_instance = auth_factory.get_auth_instance("form")
        assert.is.equal(auth_instance, require "form_auth")
    end)

    it("should throw error for invalid auth type", function()
        assert.has_error(function()
            auth_factory.get_auth_instance("invalid")
        end, "Invalid authentication type: invalid")
    end)
end)

#!/bin/bash

# NOTE: nginx.confのlua_package_pathをコピペ LUA_PATHの設定が必要
export LUA_PATH="/usr/local/openresty/lualib/?.lua;/usr/local/openresty/luajit/libs/?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/jit/?.lua;/usr/local/openresty/reverse_proxy/src/?.lua;/usr/local/openresty/reverse_proxy/src/auth/?.lua;/usr/local/openresty/lualib/resty/?.lua;/usr/local/openresty/lualib/ngx/?.lua;/usr/local/openresty/?.lua;/usr/local/openresty/luajit21/share/lua/5.1/?.lua"

pushd ../
    # NOTE: ffiはluajitでのみしかサポートされていないため,https://github.com/lunarmodules/busted/issues/369 を参考に修正
    /usr/local/openresty/luajit21/bin/busted --helper=/usr/local/openresty/reverse_proxy/tests/helper.lua -p _test tests
popd

感想

  • OpenRestyが絡むと,ngxをモックしないといけないのが辛い。testコードを書く工数があがりそうなら,結合テストにある程度寄せる判断をしても良いかもしれない。
  • なるべくモックしなくても良いようにファイルをわける設計にすることでテストが書きやすくなりそう
GitHubで編集を提案

Discussion

ログインするとコメントできます