Newly faced with the prospect of testing what will be a public library with a defined interface, I struggled for a bit with how to test the library internals (i.e. those bits not included in what would be defined in an
.mli file) without resorting to putting those internals in a separate library. In the interim, I simply avoided having an
.mli to delay having to deal with the question.
Thankfully, I eventually came across this thread/post:
To spell things out explicitly, this works like so (example file paths in the preceding comments):
(* src/dune *) (library (name lib)) ; (wrapped true) is the default (* src/impl.ml *) (* your library implementation goes here in full *) (* src/lib.ml *) include Impl (* test/test.ml *) module Lib = Lib__Impl (* your tests here, which can now use everything in Impl, regardless of the contents of any src/lib.mli that might exist *)
What’s going on is that the default “wrapping” of the library mangles the public names of all its modules, by default aliasing them into a generated module of the same name as the library. However, you can provide that “main” module (as above), and refer to the mangled names as needed to match any defined interface, or to access hidden modules and definitions in test contexts.
(IMO, this topic deserves proper treatment in either documentation for
wrapping or the more narrative sections on testing, as I suspect few are aware! Hopefully it not being particularly documented doesn’t mean this technique is subject to breakage in the future…)
Note that the internal modules’ mangled names do not show up in code completion suggestions in e.g. vscode, I suspect because they are marked as “private” via an advisory mechanism as suggested in Properly wrap a package’s modules with dune