Files
ob-elixir/docs/02-testing-emacs-elisp.md

16 KiB

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

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

;;; 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

;; 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

;; 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

;; 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

;; In Cask file or package installation
(depends-on "buttercup")

;; Or via use-package
(use-package buttercup :ensure t)

Basic Structure

;;; 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

;; 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

# 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

;;; 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

(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

(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

(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

(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

(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

(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

(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

# .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

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 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))
# 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

; Cask file
(source gnu)
(source melpa)

(package-file "ob-elixir.el")

(development
 (depends-on "ert")
 (depends-on "buttercup")
 (depends-on "package-lint"))
# 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