627 lines
16 KiB
Markdown
627 lines
16 KiB
Markdown
# Testing Emacs Lisp Packages
|
|
|
|
This document covers testing strategies and tools for Emacs Lisp packages, with focus on org-babel language implementations.
|
|
|
|
## Table of Contents
|
|
|
|
- [Testing Frameworks Overview](#testing-frameworks-overview)
|
|
- [ERT (Emacs Lisp Regression Testing)](#ert-emacs-lisp-regression-testing)
|
|
- [Buttercup](#buttercup)
|
|
- [Test Organization](#test-organization)
|
|
- [Testing org-babel Specifics](#testing-org-babel-specifics)
|
|
- [Mocking and Stubbing](#mocking-and-stubbing)
|
|
- [CI/CD Integration](#cicd-integration)
|
|
- [Build Tools](#build-tools)
|
|
- [References](#references)
|
|
|
|
---
|
|
|
|
## Testing Frameworks Overview
|
|
|
|
| Framework | Style | Best For | Included |
|
|
|---------------|------------------|-------------------------|------------------------|
|
|
| **ERT** | xUnit | Unit tests, built-in | Yes, in Emacs |
|
|
| **Buttercup** | BDD/RSpec | Readable specs, mocking | No, install from MELPA |
|
|
| **Ecukes** | Cucumber/Gherkin | Integration/acceptance | No, install from MELPA |
|
|
|
|
For a package like ob-elixir, **ERT** is recommended as it:
|
|
- Comes built into Emacs
|
|
- Is well-documented
|
|
- Works well with org-mode's own test patterns
|
|
- Integrates easily with CI
|
|
|
|
---
|
|
|
|
## ERT (Emacs Lisp Regression Testing)
|
|
|
|
### Basic Test Structure
|
|
|
|
```elisp
|
|
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
|
|
|
(require 'ert)
|
|
(require 'ob-elixir)
|
|
|
|
;; Simple test
|
|
(ert-deftest ob-elixir-test-basic-evaluation ()
|
|
"Test basic Elixir code evaluation."
|
|
(should (equal "2" (ob-elixir-evaluate "1 + 1" '()))))
|
|
|
|
;; Test with setup
|
|
(ert-deftest ob-elixir-test-variable-injection ()
|
|
"Test that variables are properly injected."
|
|
(let ((params '((:var . ("x" . 5)))))
|
|
(should (equal "10"
|
|
(ob-elixir-evaluate "x * 2" params)))))
|
|
|
|
;; Test for expected errors
|
|
(ert-deftest ob-elixir-test-syntax-error ()
|
|
"Test that syntax errors are handled."
|
|
(should-error (ob-elixir-evaluate "def foo(" '())
|
|
:type 'ob-elixir-execution-error))
|
|
|
|
;; Test with negation
|
|
(ert-deftest ob-elixir-test-not-nil ()
|
|
"Test result is not nil."
|
|
(should-not (null (ob-elixir-evaluate ":ok" '()))))
|
|
```
|
|
|
|
### ERT Assertions
|
|
|
|
```elisp
|
|
;; Basic assertions
|
|
(should FORM) ; FORM should be non-nil
|
|
(should-not FORM) ; FORM should be nil
|
|
(should-error FORM) ; FORM should signal an error
|
|
(should-error FORM :type 'error-type) ; Specific error type
|
|
|
|
;; Equality checks
|
|
(should (equal expected actual))
|
|
(should (string= "expected" actual))
|
|
(should (= 42 actual)) ; Numeric equality
|
|
(should (eq 'symbol actual)) ; Symbol identity
|
|
(should (eql 1.0 actual)) ; Numeric with type
|
|
|
|
;; Pattern matching
|
|
(should (string-match-p "pattern" actual))
|
|
(should (cl-every #'integerp list))
|
|
|
|
;; With custom messages (ERT 28+)
|
|
(ert-deftest example-with-message ()
|
|
(let ((result (some-function)))
|
|
(should (equal expected result)
|
|
(format "Expected %s but got %s" expected result))))
|
|
```
|
|
|
|
### Test Fixtures
|
|
|
|
```elisp
|
|
;; Using let for setup
|
|
(ert-deftest ob-elixir-test-with-fixture ()
|
|
(let ((org-babel-default-header-args:elixir '((:results . "value")))
|
|
(test-code "Enum.sum([1, 2, 3])"))
|
|
(should (equal "6" (ob-elixir-evaluate test-code '())))))
|
|
|
|
;; Shared setup with macros
|
|
(defmacro ob-elixir-test-with-temp-buffer (&rest body)
|
|
"Execute BODY in a temporary org buffer."
|
|
`(with-temp-buffer
|
|
(org-mode)
|
|
,@body))
|
|
|
|
(ert-deftest ob-elixir-test-in-org-buffer ()
|
|
(ob-elixir-test-with-temp-buffer
|
|
(insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC")
|
|
(goto-char (point-min))
|
|
(forward-line 1)
|
|
(should (equal "2" (org-babel-execute-src-block)))))
|
|
|
|
;; Setup/teardown pattern
|
|
(defvar ob-elixir-test--saved-command nil)
|
|
|
|
(defun ob-elixir-test-setup ()
|
|
"Setup for ob-elixir tests."
|
|
(setq ob-elixir-test--saved-command ob-elixir-command))
|
|
|
|
(defun ob-elixir-test-teardown ()
|
|
"Teardown for ob-elixir tests."
|
|
(setq ob-elixir-command ob-elixir-test--saved-command))
|
|
|
|
(ert-deftest ob-elixir-test-with-setup-teardown ()
|
|
(ob-elixir-test-setup)
|
|
(unwind-protect
|
|
(progn
|
|
(setq ob-elixir-command "/custom/path/elixir")
|
|
(should (string= "/custom/path/elixir" ob-elixir-command)))
|
|
(ob-elixir-test-teardown)))
|
|
```
|
|
|
|
### Running ERT Tests
|
|
|
|
```elisp
|
|
;; Interactive (in Emacs)
|
|
M-x ert RET t RET ; Run all tests
|
|
M-x ert RET ob-elixir RET ; Run tests matching "ob-elixir"
|
|
M-x ert-run-tests-interactively
|
|
|
|
;; From command line
|
|
emacs -batch -l ert -l ob-elixir.el -l test-ob-elixir.el \
|
|
-f ert-run-tests-batch-and-exit
|
|
|
|
;; With specific selector
|
|
emacs -batch -l ert -l test-ob-elixir.el \
|
|
--eval "(ert-run-tests-batch-and-exit 'ob-elixir-test-basic)"
|
|
```
|
|
|
|
---
|
|
|
|
## Buttercup
|
|
|
|
Buttercup provides RSpec-style BDD testing with better mocking support.
|
|
|
|
### Installation
|
|
|
|
```elisp
|
|
;; In Cask file or package installation
|
|
(depends-on "buttercup")
|
|
|
|
;; Or via use-package
|
|
(use-package buttercup :ensure t)
|
|
```
|
|
|
|
### Basic Structure
|
|
|
|
```elisp
|
|
;;; test-ob-elixir-buttercup.el --- Buttercup tests -*- lexical-binding: t; -*-
|
|
|
|
(require 'buttercup)
|
|
(require 'ob-elixir)
|
|
|
|
(describe "ob-elixir"
|
|
(describe "basic evaluation"
|
|
(it "evaluates simple arithmetic"
|
|
(expect (ob-elixir-evaluate "1 + 1" '())
|
|
:to-equal "2"))
|
|
|
|
(it "returns nil for empty input"
|
|
(expect (ob-elixir-evaluate "" '())
|
|
:to-be nil)))
|
|
|
|
(describe "variable injection"
|
|
(before-each
|
|
(setq test-params '((:var . ("x" . 10)))))
|
|
|
|
(it "injects variables into code"
|
|
(expect (ob-elixir-evaluate "x * 2" test-params)
|
|
:to-equal "20"))
|
|
|
|
(it "handles multiple variables"
|
|
(setq test-params '((:var . ("x" . 10))
|
|
(:var . ("y" . 5))))
|
|
(expect (ob-elixir-evaluate "x + y" test-params)
|
|
:to-equal "15")))
|
|
|
|
(describe "error handling"
|
|
(it "signals error on syntax error"
|
|
(expect (ob-elixir-evaluate "def foo(" '())
|
|
:to-throw 'ob-elixir-execution-error))))
|
|
```
|
|
|
|
### Buttercup Matchers
|
|
|
|
```elisp
|
|
;; Equality
|
|
(expect value :to-equal expected)
|
|
(expect value :to-be expected) ; eq comparison
|
|
(expect value :to-be-truthy)
|
|
(expect value :to-be nil)
|
|
|
|
;; Comparison
|
|
(expect value :to-be-greater-than 5)
|
|
(expect value :to-be-less-than 10)
|
|
|
|
;; Strings
|
|
(expect string :to-match "regexp")
|
|
(expect string :to-contain-string "substring")
|
|
|
|
;; Errors
|
|
(expect (lambda () (error-func)) :to-throw)
|
|
(expect (lambda () (error-func)) :to-throw 'error-type)
|
|
|
|
;; Spies (see Mocking section)
|
|
(expect 'function :to-have-been-called)
|
|
(expect 'function :to-have-been-called-with args)
|
|
(expect 'function :to-have-been-called-times n)
|
|
```
|
|
|
|
### Running Buttercup
|
|
|
|
```bash
|
|
# With Cask
|
|
cask exec buttercup -L .
|
|
|
|
# With Eldev
|
|
eldev test buttercup
|
|
|
|
# From command line directly
|
|
emacs -batch -L . -l buttercup \
|
|
-f buttercup-run-discover
|
|
```
|
|
|
|
---
|
|
|
|
## Test Organization
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
ob-elixir/
|
|
├── ob-elixir.el
|
|
├── test/
|
|
│ ├── test-ob-elixir.el # Main test file
|
|
│ ├── test-ob-elixir-session.el # Session-specific tests
|
|
│ ├── test-ob-elixir-vars.el # Variable handling tests
|
|
│ └── resources/
|
|
│ ├── sample.org # Test org files
|
|
│ └── test-project/ # Test Mix project
|
|
├── Cask # Or Eldev file
|
|
└── Makefile
|
|
```
|
|
|
|
### Test File Template
|
|
|
|
```elisp
|
|
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
|
|
|
;;; Commentary:
|
|
|
|
;; Tests for ob-elixir package.
|
|
;; Run with: make test
|
|
;; Or: emacs -batch -l ert -l test-ob-elixir.el -f ert-run-tests-batch-and-exit
|
|
|
|
;;; Code:
|
|
|
|
(require 'ert)
|
|
|
|
;; Add source directory to load path
|
|
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
|
|
(add-to-list 'load-path (expand-file-name ".." dir)))
|
|
|
|
(require 'ob-elixir)
|
|
|
|
;;; Test Helpers
|
|
|
|
(defun ob-elixir-test-org-block (code &optional header-args)
|
|
"Create org src block with CODE and evaluate it."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert (format "#+BEGIN_SRC elixir %s\n%s\n#+END_SRC"
|
|
(or header-args "")
|
|
code))
|
|
(goto-char (point-min))
|
|
(forward-line 1)
|
|
(org-babel-execute-src-block)))
|
|
|
|
;;; Unit Tests
|
|
|
|
(ert-deftest ob-elixir-test-command-exists ()
|
|
"Test that Elixir command is accessible."
|
|
(should (executable-find ob-elixir-command)))
|
|
|
|
(ert-deftest ob-elixir-test-simple-evaluation ()
|
|
"Test simple code evaluation."
|
|
(should (equal "2" (ob-elixir-test-org-block "1 + 1"))))
|
|
|
|
;;; Integration Tests
|
|
|
|
(ert-deftest ob-elixir-integration-full-block ()
|
|
"Test full org-babel workflow."
|
|
(skip-unless (executable-find "elixir"))
|
|
(should (equal "6" (ob-elixir-test-org-block "Enum.sum([1, 2, 3])"))))
|
|
|
|
(provide 'test-ob-elixir)
|
|
;;; test-ob-elixir.el ends here
|
|
```
|
|
|
|
---
|
|
|
|
## Testing org-babel Specifics
|
|
|
|
### Testing with Real Org Buffers
|
|
|
|
```elisp
|
|
(ert-deftest ob-elixir-test-full-org-workflow ()
|
|
"Test the complete org-babel evaluation workflow."
|
|
(skip-unless (executable-find "elixir"))
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+NAME: test-block
|
|
#+BEGIN_SRC elixir :results value
|
|
Enum.map([1, 2, 3], &(&1 * 2))
|
|
#+END_SRC")
|
|
(goto-char (point-min))
|
|
(org-babel-goto-named-src-block "test-block")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should (equal '(2 4 6) result)))))
|
|
```
|
|
|
|
### Testing Variable Passing
|
|
|
|
```elisp
|
|
(ert-deftest ob-elixir-test-var-passing ()
|
|
"Test :var header argument handling."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :var x=5 :var y=10
|
|
x + y
|
|
#+END_SRC")
|
|
(goto-char (point-min))
|
|
(forward-line 1)
|
|
(should (equal "15" (org-babel-execute-src-block)))))
|
|
```
|
|
|
|
### Testing Table Input/Output
|
|
|
|
```elisp
|
|
(ert-deftest ob-elixir-test-table-input ()
|
|
"Test that tables are passed correctly."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+NAME: test-data
|
|
| a | 1 |
|
|
| b | 2 |
|
|
|
|
#+BEGIN_SRC elixir :var data=test-data
|
|
Enum.map(data, fn [k, v] -> {k, v * 2} end)
|
|
#+END_SRC")
|
|
(goto-char (point-min))
|
|
(search-forward "BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should (equal '(("a" 2) ("b" 4)) result)))))
|
|
```
|
|
|
|
### Testing Session Mode
|
|
|
|
```elisp
|
|
(ert-deftest ob-elixir-test-session-persistence ()
|
|
"Test that session maintains state between evaluations."
|
|
(skip-unless (executable-find "iex"))
|
|
(unwind-protect
|
|
(progn
|
|
;; First block: define variable
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :session test-session
|
|
x = 42
|
|
#+END_SRC")
|
|
(goto-char (point-min))
|
|
(forward-line 1)
|
|
(org-babel-execute-src-block))
|
|
|
|
;; Second block: use variable
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :session test-session
|
|
x * 2
|
|
#+END_SRC")
|
|
(goto-char (point-min))
|
|
(forward-line 1)
|
|
(should (equal "84" (org-babel-execute-src-block)))))
|
|
;; Cleanup
|
|
(when-let ((buf (get-buffer "*elixir-test-session*")))
|
|
(kill-buffer buf))))
|
|
```
|
|
|
|
---
|
|
|
|
## Mocking and Stubbing
|
|
|
|
### ERT with cl-letf
|
|
|
|
```elisp
|
|
(require 'cl-lib)
|
|
|
|
(ert-deftest ob-elixir-test-mock-shell-command ()
|
|
"Test with mocked shell command."
|
|
(cl-letf (((symbol-function 'shell-command-to-string)
|
|
(lambda (cmd) "42")))
|
|
(should (equal "42" (ob-elixir-evaluate "anything" '())))))
|
|
|
|
(ert-deftest ob-elixir-test-mock-process ()
|
|
"Test with mocked external process."
|
|
(cl-letf (((symbol-function 'call-process)
|
|
(lambda (&rest _args) 0))
|
|
((symbol-function 'org-babel-eval)
|
|
(lambda (_cmd body) (format "result: %s" body))))
|
|
(should (string-match "result:" (ob-elixir-evaluate "code" '())))))
|
|
```
|
|
|
|
### Buttercup Spies
|
|
|
|
```elisp
|
|
(describe "ob-elixir with mocks"
|
|
(before-each
|
|
(spy-on 'shell-command-to-string :and-return-value "mocked-result")
|
|
(spy-on 'message))
|
|
|
|
(it "uses shell command"
|
|
(ob-elixir-evaluate "code" '())
|
|
(expect 'shell-command-to-string :to-have-been-called))
|
|
|
|
(it "passes correct arguments"
|
|
(ob-elixir-evaluate "1 + 1" '())
|
|
(expect 'shell-command-to-string
|
|
:to-have-been-called-with
|
|
(string-match-p "elixir" (spy-calls-args-for
|
|
'shell-command-to-string 0))))
|
|
|
|
(it "can call through to original"
|
|
(spy-on 'some-func :and-call-through)
|
|
;; This will call the real function but track calls
|
|
))
|
|
```
|
|
|
|
### Testing Without External Dependencies
|
|
|
|
```elisp
|
|
(ert-deftest ob-elixir-test-without-elixir ()
|
|
"Test behavior when Elixir is not installed."
|
|
(cl-letf (((symbol-function 'executable-find)
|
|
(lambda (_) nil)))
|
|
(should-error (ob-elixir-check-installation)
|
|
:type 'user-error)))
|
|
```
|
|
|
|
---
|
|
|
|
## CI/CD Integration
|
|
|
|
### GitHub Actions Workflow
|
|
|
|
```yaml
|
|
# .github/workflows/test.yml
|
|
name: Tests
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master]
|
|
pull_request:
|
|
branches: [main, master]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
emacs-version: ['27.2', '28.2', '29.1']
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Emacs
|
|
uses: purcell/setup-emacs@master
|
|
with:
|
|
version: ${{ matrix.emacs-version }}
|
|
|
|
- name: Set up Elixir
|
|
uses: erlef/setup-beam@v1
|
|
with:
|
|
elixir-version: '1.15'
|
|
otp-version: '26'
|
|
|
|
- name: Install Eldev
|
|
run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh
|
|
|
|
- name: Run tests
|
|
run: eldev test
|
|
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Set up Emacs
|
|
uses: purcell/setup-emacs@master
|
|
with:
|
|
version: '29.1'
|
|
|
|
- name: Install Eldev
|
|
run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh
|
|
|
|
- name: Lint
|
|
run: eldev lint
|
|
```
|
|
|
|
### Makefile
|
|
|
|
```makefile
|
|
EMACS ?= emacs
|
|
BATCH = $(EMACS) -Q -batch -L .
|
|
|
|
.PHONY: test test-ert compile lint clean
|
|
|
|
all: compile test
|
|
|
|
compile:
|
|
$(BATCH) -f batch-byte-compile ob-elixir.el
|
|
|
|
test: test-ert
|
|
|
|
test-ert:
|
|
$(BATCH) -l ert -l test/test-ob-elixir.el \
|
|
-f ert-run-tests-batch-and-exit
|
|
|
|
lint:
|
|
$(BATCH) -l package-lint \
|
|
--eval "(setq package-lint-main-file \"ob-elixir.el\")" \
|
|
-f package-lint-batch-and-exit ob-elixir.el
|
|
|
|
clean:
|
|
rm -f *.elc test/*.elc
|
|
```
|
|
|
|
---
|
|
|
|
## Build Tools
|
|
|
|
### Eldev (Recommended)
|
|
|
|
```elisp
|
|
; Eldev file
|
|
(eldev-use-package-archive 'gnu)
|
|
(eldev-use-package-archive 'melpa)
|
|
|
|
; Dependencies
|
|
(eldev-add-extra-dependencies 'test 'buttercup)
|
|
|
|
; Test configuration
|
|
(setf eldev-test-framework 'ert) ; or 'buttercup
|
|
|
|
; Lint configuration
|
|
(setf eldev-lint-default '(elisp package))
|
|
```
|
|
|
|
```bash
|
|
# Common Eldev commands
|
|
eldev test # Run tests
|
|
eldev lint # Run linters
|
|
eldev compile # Byte-compile
|
|
eldev clean # Clean build artifacts
|
|
eldev doctor # Check project health
|
|
```
|
|
|
|
### Cask
|
|
|
|
```elisp
|
|
; Cask file
|
|
(source gnu)
|
|
(source melpa)
|
|
|
|
(package-file "ob-elixir.el")
|
|
|
|
(development
|
|
(depends-on "ert")
|
|
(depends-on "buttercup")
|
|
(depends-on "package-lint"))
|
|
```
|
|
|
|
```bash
|
|
# Common Cask commands
|
|
cask install # Install dependencies
|
|
cask exec ert-runner # Run ERT tests
|
|
cask exec buttercup -L . # Run Buttercup tests
|
|
cask build # Byte-compile
|
|
```
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [ERT Manual](https://www.gnu.org/software/emacs/manual/html_node/ert/)
|
|
- [Buttercup Documentation](https://github.com/jorgenschaefer/emacs-buttercup)
|
|
- [Eldev Documentation](https://github.com/doublep/eldev)
|
|
- [Cask Documentation](https://github.com/cask/cask)
|
|
- [GitHub Actions for Emacs](https://github.com/purcell/setup-emacs)
|
|
- [Org Mode Test Suite](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/testing)
|
|
- [Package-lint](https://github.com/purcell/package-lint)
|