How to set up unit testing in 2023

I’ve reached a certain point in my personal project where I regret not having built unit tests right away. There’s too much state for me to easily test each path in the program and I want to know I’m not breaking basic things beyond the protection afforded to me by the typechecker.

Unfortunately I have very little experience in unit testing in OCaml. I’d love to hear about framework recommendations but more importantly, I’d love to see examples of how you set them up in your projects. I don’t mind placing unit tests in every file, but I’d prefer not to build them all the time if possible.

8 Likes

I feel a little silly responding with my beginner hat still on…

But you may have a look at this WIP:

In short, I’d like to contribute to the Caqti project a less than minimal example demonstrating general library usage.

What I have in mind is taking an OCaml curious developer by the hand so he can get a taste of what it’s like to talk to a database in OCaml.

I apply unit and integration testing as I go along so you may find a few ideas interesting here.

TLDR: I find ppx_assert just perfect :star_struck:

2 Likes

I’m a beginner when it comes to unit testing OCaml code too (have a little bit of experience using OUnit2 and Alcotest and preferred the latter), but I might have a small suggestion to make test-writing more a tiny bit more convenient.

The suggestion is just organising your tests like in the example set by lines 1-18 in this file, which I think is not a bad idea.

It was a bit of a pain for me to manually wire up the tests in a list like in minimal example on the Alcotest repository.

I think the reason OUnit2 and Alcotest need to have a list of tests at the end (unlike JS/.NET and some others which auto-discover tests) is because OCaml lacks runtime reflection, but I might be wrong about that.

i’m a beginner too and started with a small project and did it like described here: 3.3. Unit Testing with OUnit — OCaml Programming: Correct + Efficient + Beautiful so i am very interested in how to do it right. my project https://github.com/erde74/valhalla/tree/master/midgard/test

1 Like

I think ppx_expect and ppx_inline_test are the easiest way to add unit tests. You can include them side-by-side with the code they test, and you can include tests of functions that are not exposed in the mli.

If you don’t want to compile the tests along with the code, extract the tests into a “test only” library that depends on your main library. The one downside of doing this is you can only test your public interfaces.

4 Likes

Thank you for the reference. Such a good book.

I was wondering about that. We don’t have OOP’s friend mechanism to bypass class privacy restrictions. I think I’ll try those out first.

I sometimes add a Private module to expose things I want to test but don’t want to add to the “public” api.

I’m also experimenting with OCaml unit testing.

I think Alcotest is fine, but not as simple as I’d like. And it doesn’t do very much for me other than report the totals. So for my current experiment I’m just constructing my own list of (test_name, test_fun) and have a very small expect_eq function and test runner. The expect_eq takes a pair of (equal, pp) functions, just like Alcotest.check. Unlike Alcotest, I’m just using a tuple of functions instead of a first class module, which I think is overkill for this.

The largest amount of code (edit: in the test itself) is constructing the pp (formatter) that you need for every data type you’re comparing. This is true for Alcotest and ppx_expect, and I suspect for all unit testing. It is the Fmt module combinators, not the test framework, that you really need to leverage. That’s why I don’t think I really need to depend on a test framework.

I tried ppx_expect and liked it at first because it is so easy. But I quickly became disenchanted with having to manually print a string for every simple value I want to check. I prefer using checks that make use of the equal function for the data type and call the pp function for me.

I didn’t try ppx_inline_test because I don’t see a need for a ppx just to add a named test to a list. If I did want to explore the use of a ppx further, I would look at ppx_assert and specifically its test_eq to see if it made the formatter construction any easier. But I prefer not to depend on the Core/Base libraries if I don’t have to.

3 Likes

For my projects, I previously used Alcotest but switched to just using Dune’s built-in testing that works by diffing each test executable’s output with a .expected file. If you don’t mind using your pp function’s output as an equality check, then it works great. The end result is basically the same for me, but it’s simpler and I get access to nice features like dune promote.

I haven’t done a lot of real “unit” testing though, at least not by testing individual functions in each module. This may or may not be ideal for unit testing specifically, but IMO it’s nicer to use than Alcotest in general.

2 Likes

I agree that dune promote is really nice, with regular test executables like you’re using and with ppx_expect (works the same way there). If I had only a couple hours to write some tests, I would do this.

1 Like

ppx_expect has been mentioned, but you may enjoy this post from Jane Street’t blog about using it. (Here are two more nice but older posts: Testing with expectations, Repeatable exploratory programming).

It “just works” with dune+inline_tests. But I expect you’re not a base/core user, so you’d have to derive your “show” functions some other way than using their sexp machinery.

Edit: and also bisect_ppx for test coverage is nice. Especially if you need some help in covering already written code. (Even works with dune cram tests as well.)

4 Likes

I use ounit for something that I’m guessing are not really unit tests, so you may say I abuse it :open_mouth: But it works nicely together with dune test support and with the rather specific structure of my project (lexing / parsing).

https://git.sr.ht/~nobrowser/ocinco/tree/main/item/test


Ian

Thanks for mentioning this. I hadn’t tried it until just now when I saw this. It is so easy to do and the reports are great. Nice work, @antron, thank you!

2 Likes

My general habit is not to consider logic drafted until I’ve achieved good test coverage with unit tests. It’s a habit I picked up writing code for robot taxis.

I used ounit to achieve pretty good test coverage in Orsetto, but I’ve since switched to alcotest for my current projects. I don’t have something to publish that illustrates how I’m using it, but it’s more or less the same (except I’m building with dune instead of omake now).

I find it easier to write test cases with alcotest than ounit and troubleshooting test case failures is much less unpleasant.

2 Likes

Yeah, this is true. Only complaint I have about the reports is the color scheme is terrible for people like me with deuteranopia.

Thanks. I’ve opened an issue on Bisect about that:

Bisect_ppx already has a --theme option:

       --theme=THEME (absent=auto)
           light or dark. The default value, auto, causes the report's theme
           to adapt to system or browser preferences.

Perhaps this can be extended to provide default color blind-friendly themes.

2 Likes

I did not know! I’ll see if it helps.

I use ounit a ton. I mean a ton. And it’s great. But sometimes, I want to test things in the toplevel – for instance, to know that something compiles, or that something does not compile (and if so, with what error). For PPX rewriters, that’s really useful. For that, the mdx project is … wow, sooooo great.

I am giving mdx a massive plug here, b/c I really love it. Bit-by-bit, I’m going to be doing all my examples in MDX, b/c they’ll also be basic unit-tests as a result. Again: just wonderful stuff.

The documentation is great, too.

1 Like

People may be interested in this reference work on color blindness, with suggested greatly-distinguishable colors.

3 Likes

At the whole-library level one thing you can do is use dune’s wrapped library mechanism. You can bypass it in your tests but it’s reasonable to expect your downstream users not to do so.

Here’s a library called blah. It’s made of a single module Foo.

$ cat src/foo.ml
let y = 12
let x = y + 1
$ cat src/blah.ml
module Foo : sig val x : int end = Foo
$ cat src/dune
(library
 (name blah))

Here’s a test for this library which accesses the y value. It does so by by-passing the wrapped library and accessing the internal name Blah__Foo which contains two underscores and is not meant for external use.

$ cat test/r.ml
assert (Blah__Foo.y = 12)
$ cat test/dune
(test
 (name r)
 (libraries blah))

The main idea is that the internal module has a different interface than the module inside the wrapped library. (E.g., Blah.Foo.y gives you Unbound value error.)

You may end up needing a lot of duplication of interface depending how you set things up. You might need to reorganise some stuff and explicitely declare module types to avoid this.

1 Like