What's the best practice to test an async function?

Since the Testing section of RWO 2nd is still in WIP status , and it might be not easy to find unit test tutorial for OCaml project using Async lib, I put the example here. Let me know if I was wrong.

So, here’s the hello.ml lib that I want to test:

open! Core
open! Async

let get_req simple = Deferred.return simple

let%expect_test "async resp" =
  let%bind res = get_req "a" in
  Async.print_string res;
  [%expect {|a|}]

With dune :

(library
 (libraries core async expect_test_helpers_async expect_test_helpers_core)
 (inline_tests)
 (preprocess
  (pps ppx_jane))
 (name hello))

The result of dune cmd:

$ dune runtest
Done: 34/36 (jobs: 1)⏎

It seems that it works, but it looks like kind of ugly in this case. I was wondering what the best practice to test the async function?

I suppose that we should create a new folder named test, and put a test.ml and dune in it. With the test.ml like:

let free () = print_endline "freeing all resources"; Lwt.return ()

let test_lwt switch () =
  Lwt_switch.add_hook (Some switch) free;
  Lwt.async (fun () -> failwith "All is broken");
  Lwt_unix.sleep 10.

let () =
  Lwt_main.run @@ Alcotest_lwt.run "foo" [
    "all", [
      Alcotest_lwt.test_case "one" `Quick test_lwt
    ]
  ]

It just really hard to found examples about Async.

What you’ve written there seems pretty normal to me. Maybe you could say more about what you think is ugly about it.

It can certainly be a good idea to separate tests into a different library. Some reasons for this are given here.

I agree that it’s not super easy to find examples of this sort of thing with Async specifically (though I don’t think it’s too much different from non-Async expect tests). One not-very-involved example is for cohttp_async_websocket. async_smtp has a more extensive test suite in case that’s useful.

1 Like

Thanks for your share, it looks like weird to me that we should print the result and check if it’s what we expected.

I believe that this kind of unit test would be better:

open OUnit2

let get_incr a = a + 1

let test_incr _ =
  assert_equal 1 (get_incr 0)

The big advantage of this style is that the initial expected value can be populated automatically by Dune when it first runs the test. This cuts down on the upfront cost of writing each test case. For instance, see this comparison between let%expect_test and let%unit_test.


If you don’t like the expect test style, it’s perfectly possible to write unit-tests of Async code using something like the Alcotest_lwt example you gave:

open! Core
open! Async

let test_get_req () = get_req "a" >>| Alcotest.(check string) "req a" "a"

let () =
  don't_wait_for
    (Alcotest_async.run __FILE__
       [ ("all", [ Alcotest_async.test_case "get_req" `Quick test_get_req ]) ]);
  never_returns (Scheduler.go ())

This is quite boilerplate heavy, and alcotest-async requires some work to improve usability, but there is ongoing work to fix both of those problems.

3 Likes

Additionally, expect tests will catch any changes to anything that you print, whether or not that change involves a property that you would have thought to check on your own. This isn’t a silver bullet: you still have to decide what to print. And it can mean that you have to read a bunch of changes that don’t affect the correctness of your code, which in the worst case means you might be more likely to miss the changes that do matter. But I think you do get some benefit from not having to think as much about what you’d like to capture in the tests.

Along the same lines, if you like expect tests they also work just as well when you aren’t testing Async code.

2 Likes