Common Lisp中用fiveam测试CLI参数解析的最佳实践及实现咨询
问题
我正在使用fiveam测试包,想要测试由main.lisp脚本处理的CLI参数解析逻辑,但负责解析参数的main函数不接收任何参数。因此我有以下疑问:
- 在该场景下,测试命令行参数解析的最佳实践是什么?是在测试函数运行时修改命令行参数,还是修改main.lisp中的main函数使其接收CLI参数作为入参?
- 若选定方案,具体该如何实现?
- 如何模拟传入"--interface wlo1"参数,来测试main-missing-arg-p中的main函数?
main.lisp
(in-package :cl-user) (defpackage kafar (:use :cl) (:export main)) (in-package :kafar) ;;; CLI Parser (opts:define-opts (:name :port :description "port on the loopback interface on which the proxy server shall run. Recommended: 1080" :short #\p :long "port" :required t :arg-parser #'parse-integer ) (:name :interface :description "name of the interface kafar shall listen on" :short #\i :required t :long "interface" :arg-parser #'identity) (:name :help :description "print this help text" :short #\h :long "help")) (defun unknown-option (condition) ...) (defun missing-arg (condition) ...) (defmacro when-option ((options opt) &body body) `...) (defun main () (multiple-value-bind (options free-args) (handler-case (handler-bind ((opts:unknown-option #'unknown-option)) (opts:get-opts)) (opts:missing-arg (condition) (format t "fatal: option ~s needs an argument!~%" (opts:option condition)) (uiop:quit)) (opts:arg-parser-failed (condition) (format t "fatal: cannot parse ~s as argument of ~s.~%" (opts:raw-arg condition) (opts:option condition)) (uiop:quit)) ... (when-option (options :port) (let ((interfacei (getf options :interface)) (porti (getf options :port))) (kafar/proxy:proxy-server porti interfacei)) (uiop:quit) )))
test-main.lisp
(in-package :kafar/tests) ;;; Testing Proxy server (def-suite* parse-suite :description "Test parsing of CLI arguments" :in kafar-suite) ;; test the stdout without arguments (test main-help ; call main function without aguments (print (unix-opts:argv)) ; => ("sbcl") (let ((output (with-output-to-string (*error-output*) (kafar:main)))) (is (string= "warning: missing required options: \"--port\", \"--interface\"" output)))) ;; test stderr missing port argument (test main-missing-arg-p ; define interface argument, not define port argument ;Potentially modify (unix-opts:argv) (sb-ext:*posix-argv*) such that they would be equal to ("sbcl" "--interface 80") ; call main function (handling CLI argument parsing) )
测试执行命令
sbcl --non-interactive --eval "(progn (ql:quickload '(fiveam usocket nibbles unix-opts trivial-coverage)) (asdf:load-asd (merge-pathnames \"kafar.asd\" (uiop:getcwd))) (asdf:test-system 'kafar/coverage))"
解答
1. 最佳实践选择
优先修改main函数,让它接收CLI参数作为入参,这是更合理的方案:
- 解耦参数来源与解析逻辑,让解析逻辑可以独立测试,不用依赖全局命令行环境
- 代码复用性更强,后续如果需要从配置文件或其他渠道传入参数,能直接调用解析逻辑
- 避免修改全局变量带来的副作用,比如测试之间的参数污染
如果暂时不想改动原代码,也可以通过修改全局命令行参数变量来测试,但这只是临时方案,长期来看重构main函数更优。
2. 具体实现方案
方案一:重构main函数(推荐)
把参数解析逻辑抽成独立函数,让main函数支持接收自定义参数:
(in-package :kafar) ;; 新增独立的参数解析函数,接收参数数组 (defun parse-cli-args (args) (handler-case (handler-bind ((opts:unknown-option #'unknown-option)) (opts:get-opts args)) ; 传入自定义参数,替代默认命令行参数 (opts:missing-arg (condition) (format t "fatal: option ~s needs an argument!~%" (opts:option condition)) (uiop:quit)) (opts:arg-parser-failed (condition) (format t "fatal: cannot parse ~s as argument of ~s.~%" (opts:raw-arg condition) (opts:option condition)) (uiop:quit)) ;; 保留原有其他异常处理逻辑 ...)) ;; 修改原main函数,默认用命令行参数,也支持传入自定义参数 (defun main (&optional args) (multiple-value-bind (options free-args) (parse-cli-args (or args (opts:argv))) ; 优先使用传入的参数,否则用默认命令行参数 (when-option (options :port) (let ((interfacei (getf options :interface)) (porti (getf options :port))) (kafar/proxy:proxy-server porti interfacei)) (uiop:quit))))
这样测试时直接给main函数传模拟参数即可,无需修改全局变量。
方案二:修改全局命令行参数(临时方案)
如果不想改动原代码,可以修改unix-opts的全局参数或CL实现的全局变量(比如SBCL的sb-ext:*posix-argv*),但要注意测试前后恢复原参数,避免影响其他测试。
3. 模拟传入"--interface wlo1"参数的测试实现
基于重构后的main函数(推荐)
直接给kafar:main传入模拟参数,同时捕获uiop:quit避免测试进程退出:
(test main-missing-arg-p (let ((output (with-output-to-string (*standard-output*) (handler-case (kafar:main '("kafar" "--interface" "wlo1")) ; 第一个元素是程序名,后面是参数 (uiop:quit () nil))))) ; 捕获退出信号,防止测试中断 (is (string= "warning: missing required options: \"--port\"" output))))
基于修改全局变量的临时方案
测试前保存原参数,测试后恢复:
(test main-missing-arg-p (let ((original-argv (unix-opts:argv))) ; 保存原参数 (unwind-protect (progn (setf (unix-opts:argv) '("sbcl" "--interface" "wlo1")) ; 设置模拟参数 (let ((output (with-output-to-string (*standard-output*) (handler-case (kafar:main) (uiop:quit () nil))))) (is (string= "warning: missing required options: \"--port\"" output)))) (setf (unix-opts:argv) original-argv)))) ; 恢复原参数,避免影响其他测试
内容的提问来源于stack exchange,提问作者Polo
相关产品推荐
相关产品推荐

