What native mechanism covers each layer of testing — unit, integration, functional, UI, acceptance/BDD, system/smoke, and regression — for a GNUstep/AppKit stack. Specifically: what UnitKit and the GNUstep Testing framework cover (unit/integration), what StepTalk and Distributed Objects cover (functional/integration driving), where UI/acceptance testing has no native answer (GNUstep has no working accessibility layer), and how the current gershwin-on-nextbsd screenshot gate fits (system/smoke). Ends with a proposed native Gherkin/BDD framework and a full-pyramid strategy that runs in GitHub Actions and locally.
Output of a multi-agent investigation into GNUstep-native testing plus a live read of the Gershwin ISO test scripts. Every capability claim is grounded in a cited source (§11) or actual repo code. Terminology note: there is no "UIKit" in GNUstep — UIKit is Apple's iOS framework; GNUstep's GUI framework is AppKit (libs-gui). The similarly-named UnitKit is Étoilé's unit-test framework (§2) — a different thing entirely.
First, a plain taxonomy (your categories, lightly standardized — they were close):
| Category | Question it answers | Scope |
|---|---|---|
| Unit | Does this one class/method behave? | 1 object, in-process, no I/O |
| Integration | Do these components work together? | ≥2 components / processes (e.g. a framework + its deps, or DO across processes) |
| Functional | Does a feature work end-to-end at the behavior/API level? | A whole feature, often headless (no GUI needed) |
| UI | Does the actual GUI respond to clicks/keys correctly? | Real widgets on a real (or virtual) display |
| Acceptance / BDD | Does it do what a human spec says, in plain language? | Feature described in Given/When/Then; usually rides on functional or UI |
| System / smoke | Does the whole thing boot and come up? | The full ISO in a VM |
| Regression | Did a past bug come back? | Cross-cutting — any of the above, re-run on every change; visual regression = golden-image diff |
The coverage matrix — the core answer. ✔ = strong native fit · ~ = possible/partial · ✗ = not this mechanism.
| Mechanism (native unless noted) | Unit | Integ. | Funct. | UI | Accept. | System | Regr. |
|---|---|---|---|---|---|---|---|
GNUstep Testing framework (make check) | ✔ | ✔ | ~ | ✗ | ✗ | ✗ | ✔ |
| Étoilé UnitKit | ✔ | ~ | ~ | ✗ | ✗ | ✗ | ✔ |
| swift-corelibs-xctest | ✔ | ~ | ~ | ✗ | ✗ | ✗ | ✔ |
| ObjC BDD-unit (Kiwi/Cedar/Specta)¹ | ✔ | ~ | ✗ | ✗ | ~ | ✗ | ~ |
| Distributed Objects (harness transport) | ✗ | ✔ | ✔ | ~² | ~ | ✗ | ~ |
| StepTalk (scripting driver) | ✗ | ✔ | ✔ | ~² | ~ | ✗ | ~ |
| NSEvent synthesis + in-app agent | ✗ | ~ | ✔ | ✔ | ✔ | ✗ | ~ |
| External X11 (xdotool/SikuliX/PyAutoGUI) | ✗ | ✗ | ~ | ✔ | ~ | ~ | ~ |
| Gherkin/BDD framework (proposed, §6) | ✗ | ~ | ✔ | ✔ | ✔ | ~ | ✔ |
| QEMU screenshot gate (have it, §7) | ✗ | ✗ | ~ | ✗³ | ~ | ✔ | ~⁴ |
ImageMagick compare (golden image) | ✗ | ✗ | ✗ | assert | ✗ | ~ | ✔⁴ |
¹ Apple/XCTest-oriented; GNUstep support unverified — treat as "maybe." ² Only by having the bridged object post synthesized NSEvents (§5). ³ Whole-screen heuristic, blind to individual widgets. ⁴ The current gate uses a colour-count heuristic, not a golden-image diff — true visual regression would add compare (§9).
| Framework | Style | What it covers | Maturity / notes |
|---|---|---|---|
| GNUstep "Testing" framework (gnustep-tests) | macro-based (PASS, PASS_EQUAL, PASS_EXCEPTION) | Unit + integration of ObjC classes; this is the harness GNUstep itself uses to regression-test libs-base/libs-gui | Ships in gnustep-make ($GNUSTEP_MAKEFILES/TestFramework/, Testing.h, ObjectTesting.h, gnustep-tests runner); wired via the make check target. The default, canonical choice. |
| Étoilé UnitKit | xUnit (UKRunner, UKTest marker, UK* asserts) | Unit; integration/functional as far as the test body drives | Minimalist (<2000 LoC), Mac/iOS/GNUstep; Étoilé ships a GNUstep port. This is what "UnitKit" refers to — a unit framework, not "UIKit". |
| swift-corelibs-xctest | XCTest (XCTAssert*) | Unit; functional if written so | Swift-on-Foundation; runs on GNUstep Foundation but off Apple's ObjC runtime needs explicit allTests/XCTMain. No XCUITest (the GUI half is Apple-private). |
| Kiwi / Cedar / Specta+Expecta / Quick | RSpec-style BDD-unit (describe/it) | Behavior specs at the unit level | Apple/XCTest-bound; GNUstep compatibility unverified — likely needs porting. Note this is BDD at the unit level (describe/it), distinct from Gherkin acceptance specs (§6). |
make check) — it's the native default, already the regression harness for the core libraries, and needs no extra dependency. Reach for UnitKit only if you want its xUnit ergonomics across an Étoilé-style tree.Above the unit line, GNUstep's advantage is that Distributed Objects lets a test process reach into a running app and message its real objects — the native substrate for integration and functional tests. StepTalk is a scripting front-end on top of it.
NSApplication delegate under the app name by convention (the same mechanism NSServices uses), so a harness can rootProxyForConnectionWithRegisteredName:host: in and invoke methods / read state — full semantic control across a process boundary. Category: integration + functional. Gap: you drive whatever the delegate exposes; there's no standard "test object," and no known off-the-shelf DO test harness — you'd build a thin one.ScriptingInfo.plist; StepTalk can't script an un-instrumented app. Implemented and actively used (gs-desktop treats it as first-class), but niche (latest 0.10.0).NSPerformService() and read its pasteboard result. A functional/integration data path for the specific operations an app exposes as services — not a general driver.make check test (Testing framework) or drive a running app over DO/StepTalk, then assert on returned state. This is the most robust automated testing available on GNUstep today because it never touches pixels.Everything above is headless and native. The moment you want to test the GUI, you hit the wall that shapes the rest of this document.
NSAccessibility.m is ~95% the constant vocabulary (hundreds of role/attribute/notification/action name strings — real and reusable as keys) plus a handful of free functions that are all non-functional: NSAccessibilityPostNotificationWithUserInfo is an empty // FIXME (so no accessibility notifications ever fire) and the rest (UnignoredAncestor/Descendant/Children, RoleDescription, ActionDescription) just return nil. Worse, the per-widget NSAccessibility methods on the AppKit classes are only thinly present (the element classes were added ~0.29.0, largely as skeletons) — so nothing publishes a usable tree or emits change events. There is no AT-SPI2/D-Bus bridge, and Gershwin doesn't want one (§4.1). The AT-SPI ecosystem covers GTK, Java/Swing, Mozilla, OpenOffice; GNUstep is absent.AT-SPI2 and D-Bus bridges are explicitly out of scope for Gershwin. AT-SPI2 is the Linux accessibility layer, and its "2" literally means "rewritten to run over D-Bus" — it stands up a separate a11y bus plus a registry daemon (at-spi2-registryd) and marshals every widget into a language-neutral D-Bus Accessible struct. Unwanted here. The correct fix is the one macOS itself uses: NSAccessibility is a Cocoa object API and on macOS does not use D-Bus at all (it rides Apple's own IPC). Gershwin implements that same API and transports it over Distributed Objects — Cocoa-authentic, D-Bus-free, and the same object layer that powers native UI/BDD testing.
NSView/NSCell/NSControl already carry role, title, value, enabled, and focus. DO vends them (or a thin facade) and a client sends real ObjC messages; you skip the entire marshalling-into-a-neutral-struct step that AT-SPI2 requires.| AT-SPI2 / D-Bus concept | Gershwin DO-native equivalent |
|---|---|
a11y bus daemon (at-spi2-registryd) | gdomap — GNUstep's existing DO name/port registry. No new bus. |
| well-known bus names | NSConnection registerName: per app (extends the delegate-vending convention apps already use) |
| Accessible object (role/state/name, marshalled) | the real NSView/NSCell/NSControl, vended — role/enabled/value/title already live on it |
D-Bus method call (Action.doAction) | an ordinary ObjC message over DO: [element performAction: …] |
| D-Bus signals (focus-/value-changed) | NSDistributedNotificationCenter, or a client callback proxy the app messages on events (DO is bidirectional); implement NSAccessibilityPostNotification to route here |
| D-Bus XML introspection | the @protocol itself — compile-time typed, no introspection needed |
| bus service activation | launchd (already in the Gershwin/NextBSD stack) |
NSAccessibility.m already declares the full constant vocabulary (roles/attributes/actions/notifications), so there's no namespace to design — only behavior to add. Two concrete tasks: (1) implement the per-widget attribute/action methods on the AppKit classes — the same view-tree walk the TestAgent already does — and (2) implement NSAccessibilityPostNotification (currently an empty // FIXME no-op) to fan out over DO / NSDistributedNotificationCenter. Then vend the tree over DO and register via gdomap. No external D-Bus dependency — the whole loop is Gershwin apps ↔ Gershwin tools, so there's nothing to shed.Two ways to drive real widgets: from inside the app (native NSEvents, semantic) or from outside at the X11/VM layer (toolkit-agnostic, pixel-based).
+[NSEvent mouseEventWithType:location:…] / +keyEventWithType:… and post via -[NSApplication postEvent:atStart:] → sendEvent:; they flow through normal hit-testing. Read the result by inspecting NSControl/NSView/model state — semantic, not pixels.| Tool | Mouse | Keyboard | Widget-target | Read screen | Runs where |
|---|---|---|---|---|---|
| XTEST / xdotool | ✔ | ✔ | window only | ✗ | live X11 / Xvfb |
| Xnee/cnee (record-replay) | ✔ | ✔ | ✗ | ✗ | live X11 / Xvfb |
| scrot / import · ImageMagick · tesseract | ✗ | ✗ | image/OCR | ✔ | anywhere (files) |
| SikuliX / PyAutoGUI (image-based) | ✔ | ✔ | by image | ✔ | live X11 / Xvfb |
| LDTP / Dogtail / Accerciser (AT-SPI) | — | — | a11y | a11y | blocked on GNUstep |
mousemove/click/type/key. Its one semantic hook is search --name for top-level windows (GNUstep sets WM_NAME). Gotcha: window-targeted input uses XSendEvent, which some GNUstep controls ignore as synthetic — prefer global XTEST injection.Xvfb (headless framebuffer) + the app + xdotool + capture (scrot) + assert (ImageMagick %k/compare, tesseract). SikuliX/PyAutoGUI add "click the thing that looks like this" via OpenCV template-match — the vision stand-in for the missing a11y tree. Add x11vnc to watch failures. Freeze theme/font/DPI or image/OCR matches drift.Human-readable acceptance tests are buildable natively, with direct prior art: Frank and Calabash did this for iOS — Gherkin .feature files on top, an in-app server that walks the view hierarchy underneath. GNUstep suits the same design, and it routes around the §4 accessibility gap:
[NSApp windows]→subview tree and reads the real NSControl/NSView objects — so "click the button titled OK" and "the field should read X" are native and semantic without AT-SPI. That same view-walking code is the natural seed of the DO-native NSAccessibility layer (§4.1) — no D-Bus.| Layer | Choice | Why |
|---|---|---|
| Human layer | Gherkin .feature | Readable by non-coders; one corpus runs in CI and locally |
| Runner | godog (Cucumber-for-Go, single static binary) or behave (Python) | Reused, not built; zero-dep binary is ideal for Actions and local. (No official C Gherkin lib, so a fully-native ObjC runner would embed a parser — reuse is pragmatic.) |
| Transport | JSON line protocol over a Unix socket | Language-neutral — the Go/Python runner never speaks ObjC DO |
| In-app TestAgent | Small GNUstep framework linked into dev/test builds | Walks the view tree, finds controls by title/tag/class, synthesizes NSEvents (§5.1), reads control state for assertions |
| Fallback | xdotool + screendump/scrot + OCR | Same steps drop to pixels for un-instrumented apps or the whole-ISO gate |
Feature: First-boot login Scenario: Admin reaches the desktop Given the Gershwin session is running When I log in as "admin" with no password Then I should see the "System Disk" icon # agent: findControlByTitle != nil (not an OCR guess)
gershwin-developer/Library/Tests/ as the shared, flavor-agnostic acceptance corpus for all ISOs.ScriptingInfo.plist, but a generic view-walking agent works on any GNUstep app unmodified — the better default.gershwin-on-nextbsd gateToday's automation is a system/smoke test: boot the ISO in QEMU, drive input through the monitor socket (no guest agent), capture the framebuffer, assert with a colour-count heuristic + OCR. It lives in .github/workflows/build.yml (test job) + three tests/*.sh scripts.
qemu-system-x86_64 -machine q35 -m 4G -smp 2 $ACCEL -bios "$OVMF" \ -cdrom "$ISO" -boot d -vga std \ -serial file:tests/serial.log \ -monitor unix:tests/mon.sock,server,nowait \ # sendkey + screendump channel -nic user,model=e1000 -display none -no-reboot # ACCEL = "-accel kvm -cpu host" if /dev/kvm else "-accel tcg -cpu qemu64"
mon() { printf '%s\n' "$*" | socat -t2 - "UNIX-CONNECT:tests/mon.sock" >/dev/null 2>&1 || true; }
key() { mon "sendkey $1"; sleep 0.2; }
colors() { identify -format '%k' "$1"; } # unique-colour count
# boot-test.sh: PASS when %k > 64 (COLOR_THRESHOLD) → greeter painted
# loginwindow: type "admin", ret, ret (NON-gating, best-effort)
# workspace-test: PASS when %k > 1500 OR tesseract finds "System Disk" (else exit 1)boot-test and workspace-test have no continue-on-error (they fail the job); loginwindow-test is continue-on-error: true. The release job carries needs: [build, test] + needs.test.result == 'success', so a failed desktop check blocks publish. The passing docs/desktop.png is committed back for the README; boot-artifacts (serial.log, frames, PPMs) upload on every run.sendkey, mouse_move (relative), mouse_button, screendump, over a socket via socat. Single-client socket — with libvirt use virsh qemu-monitor-command.mouse_move is relative (can't hit a pixel reliably). Add -device usb-tablet and switch clicks to QMP input-send-event (absolute axis + button + key). Assert with screendump + ImageMagick/tesseract. Stays no-guest-agent — the right layer for whole-ISO tests.Regression isn't a separate tool; it's running the other categories on every change and catching what came back. Native building blocks:
make check in CI — exactly how the core libraries guard themselves. Add per-component Tests/ and run on every push.compare a screenshot against a committed golden image (per-region to tolerate clocks/cursors), failing on a metric threshold. Cheap to add on top of the existing screendump capture, and it upgrades the gate from smoke to real regression.make check) for unit/integration across Gershwin components now; it's the cheapest, most robust win.compare on the ISO gate (§9).make check anywhere; godog/Xvfb per-app in seconds (a shared composite action + a local script); the QEMU gate for whole-ISO. Home the shared suites in gershwin-developer/Library/Tests/.NSAccessibility implementation transported over Distributed Objects (§4.1) — not an AT-SPI2/D-Bus bridge, which is out of scope for Gershwin. It reuses gdomap as the registry and needs no new bus; the in-app agent is both the near-term substitute and the foundation for it.gdomap (GNUstep DO name server) · macOS AXUIElement (accessibility without D-Bus).github/workflows/build.yml, tests/{boot,loginwindow,workspace}-test.sh (read live from main)Flagged/unverified: ObjC BDD-unit frameworks (Kiwi/Cedar/Specta) on GNUstep — Apple/XCTest-oriented, compatibility not confirmed. StepTalk 0.10.0 release date (NEWS undated). No existing DO/StepTalk-based GUI-test harness was found (viable, not observed). QEMU mouse_move under -device usb-tablet is version-dependent — the reliable absolute-click path is QMP input-send-event.