plugins/ocaml-dev/skills/testing/SKILL.md
Testing strategies for OCaml libraries. Use when discussing tests, alcotest, eio mocks, test structure, or test-driven development in OCaml projects.
npx skillsauth add avsm/ocaml-claude-marketplace testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use test/ directory with:
test.ml - Main runner controlling initialization ordertest_x.ml - One file per module x.ml being tested, exports suitelib/
├── foo.ml
└── bar.ml
test/
├── dune
├── test.ml # Main runner
├── test_foo.ml # suite : (string * unit Alcotest.test_case list) list
└── test_bar.ml
For single-module libraries, a single test_foo.ml as runner is acceptable.
(test
(name test)
(libraries mylib alcotest logs logs.fmt fmt.tty))
The main test.ml controls initialization order for side effects:
(* 1. Initialize RNG before any test module is loaded *)
let () = Crypto_rng_unix.use_default ()
(* 2. Set up logging *)
let () = Fmt_tty.setup_std_outputs ()
let () = Logs.set_reporter (Logs_fmt.reporter ())
let () = Logs.set_level (Some Logs.Debug)
(* 3. Run all test suites *)
let () = Alcotest.run "mylib" Test_foo.suite
For multiple modules:
let () = Crypto_rng_unix.use_default ()
let () = Alcotest.run "mylib" (Test_foo.suite @ Test_bar.suite)
Each module exports a suite value. Do not initialize RNG or run Alcotest here.
(** Tests for Foo module. *)
let test_basic () =
let result = Foo.process "input" in
Alcotest.(check string) "expected output" "output" result
let test_empty () =
let result = Foo.process "" in
Alcotest.(check string) "empty input" "" result
let suite =
[
( "process",
[
Alcotest.test_case "basic" `Quick test_basic;
Alcotest.test_case "empty" `Quick test_empty;
] );
]
If a test module needs RNG at load time, use lazy evaluation:
let key = lazy (Crypto_rng.generate 32)
let key () = Lazy.force key
let test_encrypt () =
let ciphertext = Foo.encrypt ~key:(key ()) plaintext in
...
This defers RNG use until tests actually run, after test.ml initializes the RNG.
let result_testable ok_t =
Alcotest.result ok_t Alcotest.string
let my_type_testable =
Alcotest.testable My_type.pp My_type.equal
Alcotest.(check int) "count" 42 actual
Alcotest.(check string) "name" expected actual
Alcotest.(check bool) "flag" true actual
Alcotest.(check (list int)) "items" [1;2;3] actual
Alcotest.(check (option string)) "maybe" (Some "x") actual
Alcotest.(check (result int string)) "result" (Ok 42) actual
let test_raises () =
Alcotest.check_raises "should fail" (Invalid_argument "bad")
(fun () -> Foo.parse "bad")
Always initialize in test.ml before Alcotest.run:
Crypto_rng_unix.use_default () or Mirage_crypto_rng_unix.use_default ()Logs.set_reporter and Logs.set_levelThis ensures deterministic test ordering and proper side-effect sequencing.
Set up logging in tests using the standard Logs library:
let () = Fmt_tty.setup_std_outputs ()
let () = Logs.set_reporter (Logs_fmt.reporter ())
let () = Logs.set_level (Some Logs.Debug)
Default behaviour: Logs at Debug level. Alcotest captures output to a file by default, so verbose logging doesn't clutter the terminal. Output is shown only when tests fail.
For per-source control, set levels after the reporter:
let () = Logs.Src.set_level Conpool.src (Some Logs.Debug)
let () = Logs.Src.set_level Requests.src (Some Logs.Warning)
lib/ should have a corresponding test module in test/."users", "commands", "process")"basic", "empty_input", "parse_error")Function Coverage: Test all public functions exposed in .mli files, including success, error, and edge cases.
Test Data: Use helper functions to create test data:
let make_user ?(name = "test") ?(id = 1) () = User.v ~name ~id
Property-Based Testing: For complex logic, consider property-based testing with QCheck:
let test_roundtrip =
QCheck.Test.make ~count:1000
~name:"encode then decode is identity"
QCheck.string
(fun s -> Codec.decode (Codec.encode s) = s)
Cram tests verify CLI executable behavior.
Use Cram Directories: Every Cram test should be a directory ending in .t (e.g., my_feature.t/).
Create Actual Test Files: Avoid embedding code within run.t using heredocs. Create real source files within the test directory.
test/
└── my_feature.t/
├── run.t # The cram test script
├── input.txt # Test input file
└── expected.json # Expected output
# Run all tests
dune test
# Run tests and watch for changes
dune test -w
# Run a specific test
dune exec test/test.exe -- test "suite_name"
# Run tests with coverage
dune test --instrument-with bisect_ppx
bisect-ppx-report summary
tools
Working with the OxCaml extensions to OCaml. Use when the oxcaml compiler is available and you need high-performance, unboxing, stack allocation, data-race-free parallelism
development
Creating OCaml library tutorials using .mld documentation format with MDX executable examples. Use when discussing tutorials, documentation, .mld files, MDX, or interactive documentation.
development
Security hardening for OCaml libraries through systematic vulnerability research. Use when Claude needs to: (1) Research CVEs in similar implementations (C, Rust, Go, Python) and add regression tests, (2) Add fuzz tests for parsers and encoders, (3) Audit integer handling and buffer operations, (4) Test boundary conditions and malformed input, (5) Review cryptographic usage, (6) Add defensive checks against common vulnerability classes
testing
Working with IETF RFCs in OCaml projects. Use when mentioning RFC numbers, implementing internet standards, adding specification documentation, or discussing protocol compliance.