OpenResty + Luaで単体テストを書こうとした時にハマった場所まとめ

## これは何?

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

---

## 環境

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

詳しくは[自分のリポジトリ](https://github.com/RyosukeDTomita/GateKeeper)参照。

---

## ハマりポイント①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](https://openresty.org/en/luajit.html)

::: note info
実行時コンパイラ（じっこうじコンパイラ、just-in-time compiler、JITコンパイラ）とは、ソフトウェアの実行時にソースコードをコンパイルするコンパイラのこと。[Wikipedia](https://ja.wikipedia.org/wiki/%E5%AE%9F%E8%A1%8C%E6%99%82%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9)より
:::

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

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

- LuaRocksはLuaで使用するライブラリの管理ツール
- LuaRocksのインストールには[HereRocks](https://github.com/mpeterv/hererocks)を使うとLuaやLuaJITとセットでインストールできる

自分はもともと，Lua5.4を使用していましたが，[LuaJITの公式ドキュメント](https://luajit.org/luajit.html)によるとLuaJITはLua5.1をベースに作成されています。

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

```shell
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: 必要最低限を探してもいいかも

```shell
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

以下は実行例です。

```helper.lua
local ffi = require "ffi"
```

```shell
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内容を増やしてくれるで，いい時代になりましたね。

```lua
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
        }
    }
```

---

## 最後に: サンプル

### 被テストコード

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

```lua
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
```

### テストコード

```lua
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)

```

```run-test.sh
#!/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コードを書く工数があがりそうなら，結合テストにある程度寄せる判断をしても良いかもしれない。
- なるべくモックしなくても良いようにファイルをわける設計にすることでテストが書きやすくなりそう