Testing Mollymawk
2026-09-25Unikernels compile directly to specialized virtual machine images without a traditional operating system, userland, or shell. Testing them has historically presented unique challenges. How do we test a web application running as a unikernel? Over the past month, we set out to answer this question by building a test suite for Mollymawk.
1. Testing a Unikernel
In standard web applications, testing HTTP endpoints usually involves running the server on localhost and sending curl requests, or using test harness libraries that hook into the application's request pipeline.
In a MirageOS unikernel, however:
- Network stacks and block devices are parameterized over functors.
- The final artifact is a bootable binary (e.g.
mollymawk.hvt) intended to run under a hypervisor. - Running full virtualization tests inside a hypervisor for every unit test is slow, resource-heavy, and difficult to inspect in CI.
To solve this, and for a start, we did the following:
a. Decouple business logic into a native library:
We extracted Mollymawk's core logic into a reusable OCaml library (mollymawk_libraries). Previously, the code was compiled strictly as part of the unikernel target. Organizing the codebase into a library allows us to write standard native Unix test executables that link against the exact same business logic modules (storage, user management, policy validation, email, Albatross interaction). This ensures that the code being tested is identical to the code running inside the unikernel.
b. Mock external Mirage devices and run loopback services:
- For block storage and read-only key-value storage, we provide in-memory Mirage implementations (
Mock_BlockandMock_KV). - For the HTTP API, we feed requests directly in-memory to Mollymawk's request handler.
- For services that Mollymawk interacts with over a network (such as Albatross daemons and SMTP relays), we run lightweight mock services listening on
localhost.
c. Unit and Integration testing with Dune and Alcotest: Using Alcotest and Dune, we executed unit and integration tests locally and in CI.
2. Verifying Disk Storage and Schema Backward Compatibility
Mollymawk persists sensitive state—including user credentials, access control policies, API tokens, scaling policies, and email settings—to disk using oneffs (One File FileSystem) over a Mirage block device.
Data corruption or subtle serialization bugs on disk could lock administrators out of their infrastructure. Mollymawk has also undergone several changes to it's data format, so testing for backwards compability enables us to perform data migrations and code refactorings without much worry.
3. Testing HTTP Endpoints
Mollymawk defines 66 distinct HTTP routes. These range from public landing and login pages to administrative API endpoints that trigger VM deployments, update scaling policies, manage block storage, or stream console logs.
We wrote tests covering all 66 routes.
Testing web endpoints requires more than just testing the "happy path". To prevent security regressions and ensure consistent behavior, every endpoint in Mollymawk is evaluated against a comprehensive testing matrix:
-
Authentication: Does an unauthenticated request get properly rejected (HTTP 401 Unauthorized for API endpoints, or redirected to
/sign-infor browser views)? -
CSRF Protection: Are state-changing POST requests protected by valid CSRF tokens? Do requests with missing or expired tokens fail with HTTP 403 Forbidden?
-
Authorization & RBAC: Can regular users access administrative endpoints (e.g. activating user accounts, creating other users, or overriding global Albatross configurations)?
-
Input Validation: What happens when required JSON or form fields are missing, malformed, or contain invalid types (e.g. invalid IP addresses or negative CPU quotas)?
-
HTTP Method Enforcement: Does sending a
GETrequest to aPOST-only endpoint return HTTP 405 Method Not Allowed or HTTP 400 Bad Request?
Structuring Endpoint Tests with Alcotest
Using Alcotest, we organized our tests by functional domains:
-
Authentication & Sessions: Login forms, session cookies, logout revocation, and password hashing.
-
API Tokens: Creating, listing, and revoking scoped bearer tokens used for automated API access.
-
User Management: Admin endpoints for user creation, updating passwords, and access restrictions.
-
Unikernel Management & Console Streaming: Querying unikernel status, HTML dashboard views, and streaming console history.
-
Block Devices & Storage Operations: Creating, provisioning, attaching, and destroying Mirage block devices.
-
Albatross Remote Management: Testing TLS communication with Albatross daemons, verifying command dispatching and error handling.
-
Email & Policies: Updating SMTP relay configurations, sending test emails, verifying authentication tokens via email, and enforcing resource allocation policies.
-
Monitoring, Updates, Rollbacks & Autoscaling: Configuring autoscaling policies, comparing binaries for updates, triggering rollbacks, and updating metrics scrapers.
In total, 282 individual test cases which complete in under 7 seconds.
Testing `Mollymawk data serialization tests for storage'.
Test Successful in 0.003s. 17 tests run.
Testing `Mollymawk API Function & Data Format Tests'.
Test Successful in 6.807s. 265 tests run.
4. Tracking Code Coverage with Bisect_ppx_ng
To ensure our tests were actively exercising critical branches and error handlers, we integrated bisect_ppx_ng into our Dune build workflow.
Bisect provides detailed code coverage statistics:
- Overall project coverage: 74.13% (8,902 / 12,009 points).
- Unikernel entrypoint (
unikernel.ml): 56.95% (1,249 / 2,193 points).
The remaining uncovered points in unikernel.ml represent code which executes when booting inside a real virtual machine hypervisor, whereas the HTTP routing logic, request dispatching, and error handlers are exercised by our native test harness.
While there's no end to how much tests can be written, the test suite as of now covers a lot of areas and is a good starting point for mollymawk.

5. Some issues Discovered While Writing Tests
Writing this test suite was not only about asserting that existing code worked; it also flushed out a variety of subtle bugs and edge cases:
-
List Ordering in Deserialization: While verifying disk storage roundtrips, we uncovered that JSON list decoders accumulated items in reverse order. Without a subsequent
List.rev, repeated serialization and deserialization cycles could invert the order of user records or configuration entries. -
Downloading block devices: The endpoint for downloading block devices didn't properly check if the albatross instance exists before processing the download request.
-
Deleting nonexistent tokens: Sending a delete requests for a token which doesn't exist returns a 202 Success response instead of a 404 not found response.
6. Continuous Integration with Forgejo on FreeBSD
Having a test suite is only half the battle; it must run automatically on every pull request to catch regressions before they reach the main branch.
As part of migrating our repositories to git.robur.coop (powered by Forgejo), we set up automated CI running on our own infrastructure: a dedicated FreeBSD 15.1 virtual machine runner running Forgejo Actions (forgejo-act_runner).
This CI runner executes dune runtest and checks code formatting with dune build @fmt on every pull request:

6. Conclusion
Mollymawk now has:
- Storage integrity verification across block operations and file-backed stores.
- 66 HTTP endpoints verified across 282 automated unit tests.
- Code coverage reporting via
bisect_ppx_ng. - Automated CI runs on every pull request using Forgejo on FreeBSD.
The work to build the test suite and CI was completed across the following pull requests:
- Modularization:
- Extract core files into
mollymawk_libraries: PR #274
- Extract core files into
- Storage Testing:
- Authentication & Endpoint Testing:
- Unit tests for pure authentication functions: PR #279
- Mock devices and authentication endpoints: PR #281
- Admin endpoint tests: PR #282
- Login and logout endpoints: PR #283
- API token management endpoints: PR #284
- Unikernel lifecycle endpoints: PR #291
- Refactor user creation in tests to use API: PR #293
- HTML view GET endpoints: PR #294
- Block operation endpoints: PR #295
- Albatross orchestration endpoints: PR #296
- Email settings and access policies: PR #297
- Monitoring, update, rollback, and scaling endpoints: PR #298
- Coverage & CI Automation:
- Add
bisect_ppx_ngfor code coverage statistics: PR #289 - Forgejo CI workflow on FreeBSD runner: robur/mollymawk#1
- Test for Static image assets: robur/mollymawk#2
- Add