ERT 测试交互命令的三种方式
目录
最近翻 Emacs 31 的 NEWS 文件 时,看到一条新增:ERT 增加了 ert-play-keys 函数,专门用来模拟用户按键。ERT 本身就有 ert-simulate-keys 和 ert-simulate-command 两个类似工具,这个新的到底解决了什么老工具解决不了的问题?顺着 NEWS 里的线索去看 源码 和 测试文件 后发现,这三个工具各有明确的适用场景,本文逐一介绍。
前置条件: 这三个工具都在 ert-x.el 里,不在 ert.el 里。ERT 自 Emacs 24.1 起就是内置的,但只 (require 'ert) 是不够的,必须额外加载扩展库:
(require 'ert-x)
ert-simulate-keys 和 ert-simulate-command 从 Emacs 24.1 就有了,而 ert-play-keys 是 Emacs 31 才新增的。
ert-simulate-keys:模拟 minibuffer 输入
ert-simulate-keys 的原理是把按键事件注入到 unread-command-events 变量中,这样当被测代码调用 read-from-minibuffer 之类从命令事件队列读输入的函数时,就会读到我们预先塞进去的内容。
(ert-deftest test-simulate-keys-for-minibuffer () "ert-simulate-keys 可以向 read-from-minibuffer 提供预设输入。" (ert-simulate-keys (listify-key-sequence "hello\n") (should (string= (read-from-minibuffer "Input: ") "hello"))))
但它的局限也很明显:它 只能 用于从 unread-command-events 读输入的函数。如果你想测试"用户按下某个键,触发了 keymap 里绑定的命令"这种场景, ert-simulate-keys 做不到——因为按键事件只是被塞进了 unread-command-events 变量,并没有一个命令循环在跑来消费它们。只有 read-from-minibuffer 这类函数会主动从 unread-command-events 取输入。
(ert-deftest test-simulate-keys-cannot-start-commands () "ert-simulate-keys 无法启动按键命令——事件只存在 unread-command-events 中。" (ert-with-test-buffer () (let (command-ran) (let* ((map (let ((map (make-sparse-keymap))) (define-key map [?b] (lambda () (interactive) (setq command-ran t))) map)) (minor-mode-map-alist (cons (cons t map) minor-mode-map-alist))) ;; 事件被放入 unread-command-events,但没有命令循环来处理它们 (ert-simulate-keys (listify-key-sequence "b") ;; body 里没有函数消费这些事件 nil)) ;; ?b 的 keymap 绑定没有被触发 (should (eq command-ran nil)))))
ert-simulate-command:手动调用指定命令
ert-simulate-command 的思路完全不同:你不需要模拟按键,而是直接告诉它"我要调用这个命令,参数是这些"。它会模拟命令循环的行为——运行 pre-command-hook 、设置 this-command 、调用命令、运行 post-command-hook ——但不会经过 keymap 查找。
(ert-deftest test-simulate-command () "ert-simulate-command 可以直接以交互方式调用命令。" (ert-with-test-buffer () (let (hook-ran) (let ((pre-command-hook (list (lambda () (setq hook-ran t))))) (ert-simulate-command (list (lambda (x) (interactive (list "test input")) (insert x) :ok) "test input"))) (should (eq hook-ran t))) (should (string= "test input" (buffer-substring (point-min) (point-max))))))
注意 ert-simulate-command 有一个重要限制:命令 不是 通过 call-interactively 调用的。这意味着如果你的命令内部调用 called-interactively-p 来判断自己是不是被交互调用的,结果会是 nil 。下面这个测试展示了这个问题:
(ert-deftest test-simulate-command-not-call-interactively () "ert-simulate-command 中 called-interactively-p 返回 nil。" (let (result) (ert-simulate-command (list (lambda () (interactive) (setq result (called-interactively-p 'any))))) ;; 不是通过 call-interactively 调用的,所以返回 nil (should (eq result nil))))
另外, ert-simulate-command 需要你明确知道要调用哪个命令。如果你需要测试"用户按下某个键之后发生了什么"(即经过 keymap 查找后的完整流程),它帮不上忙。
ert-play-keys:模拟真实的按键序列(Emacs 31 新增)
Emacs 31 新增的 ert-play-keys 填补了上面两个工具留下的空白。它的实现只有一行:
(defun ert-play-keys (keys) "Play the key sequence KEYS as if it was user input." (funcall (kmacro keys)))
它调用 kmacro 把按键序列当作键盘宏来执行。因为键盘宏走的是完整的命令循环路径——包括 keymap 查找、命令执行、hook 触发——所以它可以触发 keymap 绑定的命令,也能正确处理 called-interactively-p 。
使用时需要配合 ert-with-test-buffer 的 :selected t 参数(或 ert-with-buffer-selected ),这会在临时窗口中选中 buffer,让按键序列能正确路由到目标 buffer:
(ert-deftest test-play-keys-triggers-keymap () "ert-play-keys 可以触发 keymap 绑定的命令。" (ert-with-test-buffer (:selected t) (let (command-ran) (let* ((map (let ((map (make-sparse-keymap))) (define-key map [?b] (lambda () (interactive) (setq command-ran t) (insert "ran"))) map)) (minor-mode-map-alist (cons (cons t map) minor-mode-map-alist))) (ert-play-keys (vconcat [?b]))) (should (eq command-ran t)) (should (string= "ran" (buffer-substring (point-min) (point-max)))))))
ert-play-keys 还可以在一次调用中混合不同类型的输入——既有触发命令的按键,也有直接插入的文本:
(ert-deftest test-play-keys-mixed-input () "ert-play-keys 可以混合按键命令和文本插入。" (ert-with-test-buffer (:selected t) (let* ((map (let ((map (make-sparse-keymap))) (define-key map [?X] (lambda () (interactive) (insert "[pressed-X]"))) map)) (minor-mode-map-alist (cons (cons t map) minor-mode-map-alist))) ;; [?X] 触发 keymap 命令,"hello" 作为文本插入 (ert-play-keys (vconcat [?X] "hello"))) (should (string= "[pressed-X]hello" (buffer-substring (point-min) (point-max))))))
三种方式对比
| 特性 | ert-simulate-keys |
ert-simulate-command |
ert-play-keys |
|---|---|---|---|
| 触发 keymap 绑定 | 否 | 否 | 是 |
支持 called-interactively-p |
N/A | 否 | 是 |
运行 pre/post-command-hook |
否 | 是 | 是 |
| 模拟文本插入 | 仅通过 minibuffer | 需手动传参 | 是(直接输入) |
| 使用方式 | 宏(包裹 body) | 函数(传入命令) | 函数(传入按键序列) |
| Emacs 版本 | 一直有 | 一直有 | 31+ |
简单来说:
- 需要测试命令的 minibuffer 输入 →
ert-simulate-keys - 需要测试特定命令的执行效果(不需要经过 keymap) →
ert-simulate-command - 需要模拟用户真实按键操作 →
ert-play-keys