Using fixture files in OCaml tests with Dune
Alright, it's time for another one of those "problem I spent half and hour on with a one line solution" posts! On today's episode, Dune! Not the sci-fi franchise, but OCaml's revered build system. It's insanely fast and powerful but it has quite a steep learning curve, which is why I feel like short hands-on blog posts featuring common use-cases are pretty useful!
Introduction
For those who might be unaware, fixtures is just a fancy word for "test data". They generally refer to non-code files used during unit-testing. Here is a simple example.
Assuming you have a file called fixtures/data.txt
, you can read it in a test and use it in whatever way you'd like.
let () =
let text = In_channel.(with_open_text "fixtures/data.txt" input_all) in
assert (text = "Hello, world!")
Use-cases for this technique are numerous:
- Load records in a database through an ORM;
- Store API responses to mock HTTP requests;
- Store sample input files for integration tests...
The setup
In my use-case, I was writing a parser for JSPF, a JSON structure used to hold music playlist. The objective of my test was to
- Read a JSON string from a fixture file;
- Run it through my parser;
- Test against the properties of the loaded object.
I'm using Alcotest as a test framework, and the test looks something like this.
open Alcotest
open Xspf (* my library *)
let test_parse_json_title () =
let json = Yojson.Safe.from_file "data/playlist.json" in (* 1 *)
let parsed_playlist = Json_parser.parse_json json in (* 2 *)
check string "correct title" "JSPF example" parsed_playlist.title (* 3 *)
Provided that I do have a file data/playlist.json
that contains a JSPF playlist whose title is "JSPF example", the test should work just fine, right? Right?
The catch
Of course not, otherwise I wouldn't have written a post about it!

Like most modern OCaml projects, ocaml-xspf uses Dune as its build system, and that includes compiling and running the tests. Here's what happens when I try to run the test above.
$ dune test
┌─────────────────────────────────────────────────────────────┐
│ [FAIL] JSON Parser 1 parse_json_title │
└─────────────────────────────────────────────────────────────┘
[exception] Sys_error("data/playlist.json: No such file or directory")
This looked very weird to me, because I could not reproduce the error in a REPL session: the file was right there, how could the test not find it? I tried a bunch of things, like move the data
folder around, because maybe the test was not run from the folder with the test source code. I ended up added an instruction to the current working folder in order to get a better idea of what was going on.
print_endline (Sys.getcwd ());
And here is what came back.
/ocaml-xspf/_build/default/test
Oh, right! Because Dune compiles everything under its _build
folder, that means that the test is run from the build folder, not from the source folder! It makes perfect sense now, but remember that I work with Ruby on a daily basis, an interpreted language that does not need to be compiled.(*)
(*): Well, technically OCaml can be interpreted as well, but that's not how Dune does it.
Now, the question becomes: "How do I tell Dune to copy my fixture folder in the build folder?"
The search
For a hobbyist OCaml developer such as myself, Dune has quite a steep learning curve. Even though the documentation is outstanding, and I cannot thank the developers enough for their work on it, I cannot help but find it a bit disconnected at times. It can be hard to pinpoint exactly, but I think my troubles might stem from a lack of structure: advanced edge cases are documented right next to common entry-level things, and lack of examples sometimes make it difficult to understand how to apply this or that information.
Let's look at our current case, for example. Below is the state of the dune
file for my test, as it was when I discovered the issue with fixture files.
(test
(name test_xspf)
(libraries alcotest xspf))
Basically, it defines a test executable called test_xspf
that depends on libraries alcotest
(the testing framework) and xspf
(my own library, the one under test).
Looking at the documentation for the test
stanza, nothing looked like what I wanted. I also found the copy_files
stanza that looked promising, but ultimately, I found that it only copied the data
folder once, ignoring all future updates. I'm pretty sure I was using it wrong, but still, the documentation wasn't really helpful there.
The solution
Eventually, I stumbled upon this documentation page, titled "Dependency Specification". I was interested because I could see the fixture files as a dependency of the test, since the test requires them to work properly. And lo and behold, you can add (deps (source_tree data))
to say that something depends on all files under the data
directory. However, the documentation doesn't explain how to use this stanza.
Out of sheer coincidence, I tried adding it to the test
definition, like this.
(test
(name test_xspf)
(libraries alcotest xspf)
(deps (source_tree data)) ; <-- Here!
And you know what? It worked like a charm!

Now, the entire data
directory get copied over during build, and if a fixture file changes, it causes the test to get rerun.
Conclusion
As I said in the intro, this seems to me like a very common use-case, and maybe I'm stupid, but I had a really hard time making use of Dune's otherwise great documentation to find a solution.
Let's say this was a great opportunity for me to level-up on Dune and to take a deep dive in its docs! I have a bunch of ideas for small side-projects just like this one with OCaml, so expect me to use Dune a lot more, and I will probably write more posts about it if I encounter other interesting use-cases like this one.