-
Notifications
You must be signed in to change notification settings - Fork 80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] A nestable test API for Alcotest #294
base: main
Are you sure you want to change the base?
Conversation
f125111
to
485263a
Compare
60a8a28
to
e1eafe6
Compare
b9f71fa
to
1f9f630
Compare
216a728
to
521e29e
Compare
f123087
to
57137df
Compare
Hi. Thanks for working on this issue. The restrictions on nesting are in my mind alcotest's biggest limitation. I often try to work around this by adding a sort of namespacing on the name of the tests. In addition, if a group is a test, that means that the argument of To reiterate my comment in #275, I think it'd be very good to have some simple syntax for the common case (quick test, no explicit loc, no filter, just a unit argument) and OUnit's API is a good example of that. (that can be done as a third party library of course) |
Thanks for the feedback @emillon :-) For clarity, the current proposed type of val test :
?here:source_code_position
-> ?tags:tag_set
-> name:string
-> ('a -> unit)
-> 'a test (... though it's currently a bit obfuscated in the draft implementation.) I'm reticent to go further and drop the string name, but it's technically possible. Is your concern that the optional arguments add too much noise, or are you particularly fond of the infix operator provided by OUnit? |
a330879
to
a3a2c83
Compare
tl;dr: I suggest keeping the test list flat and have the alcotest library recognize dotted paths. I'd rather stay away from anything that makes the API more complicated. In particular:
I think we could improve the current situation by providing a simple packing function, keeping the test suite flat: (* pack "Foo.Bar.regression tests" tests *)
val pack : string -> 'a test list -> 'a test list This would be coupled with the convention that Footnotes:
|
In short, I think:
In long...
I don't agree that the proposed API is more complicated than the existing one, except in the sense that it contains more optional arguments. To take let () =
Alcotest.run "Utils"
[
( "string-case",
[
Alcotest.test_case "Lower case" `Quick test_lowercase;
Alcotest.test_case "Capitalization" `Quick test_capitalize;
] );
( "string-concat",
[ Alcotest.test_case "String mashing" `Quick test_str_concat ] );
( "list-concat",
[ Alcotest.test_case "List mashing" `Slow test_list_concat ] );
] becomes: let () =
Alcotest.run "Utils"
[
Alcotest.group "string-case"
[
Alcotest.test "Lower case" test_lowercase;
Alcotest.test "Capitalization" test_capitalize;
];
Alcotest.group "string-concat"
[ Alcotest.test "String mashing" test_str_concat ];
Alcotest.group "list-concat"
[
Alcotest.test "List mashing" test_list_concat ~tags:Alcotest.Tags.slow;
];
] This looks basically equivalent to me, with the differences being (a) any suite layout is possible, not just the previously-existing one (which was contrived to meet the pre-imposed structure anyway), and (b) the If anything, I think the proposed API is simpler than the previous one. The following simple suites are now valid: open Alcotest
let () = run "suite1" [ test "1" f; test "2" g ]
let () = run "suite1" [ test "equal" f; group "codec" [ test "encode" g; test "decode" h ] whereas before it would have been necessary to wrap the tests of depth 1 inside some singleton group. At a higher level, my experience of watching beginners interact with the Alcotest API is that they get bogged down in the concrete type aliases and imposed structure, with a thought process like:
The concrete types also make for some rather noisy and confusing type errors along the way. On the other hand, the proposed API asks the user for a list of
PPX will never be a requirement of the Alcotest API, which should always be designed for hand-written code. If the motivation for this RFC were just to provide a better set of functions for PPXes to call, it could just hide these in an As said above, I think the simpler model of test suites is a win for non-PPX users too. I also think attaching source locations is worthwhile for even for hand-written code – we've been doing this for let test_thing =
Alcotest.test ~pos:__POS__ "thing" (fun () ->
(* ... test assertions here ... *))
let () = Alcotest.run __FILE__ [ test_thing ]
Expanding the full type of an Alcotest suite, we get: type suite = (string * (string * [ `Quick | `Slow ] * (unit -> unit)) list) list This doesn't strike me as a particularly "natural" type of test suites existing in the wild: if someone puts Regardless, this PR already supports
The fact that the current API makes the As said above, the RFC proposes retaining support for the current API under |
Actually, I'd rather specify my tests in the following format, which I've done in several projects: type suite = (string * (unit -> unit)) list [The current type being type suite = (string * (string * [ `Quick | `Slow ] * (unit -> unit)) list) list ] It's easy to convert from this format to whichever format alcotest actually uses. So, I don't think there's a real issue. It would be nice if alcotest provided such converter from the simpler |
While I can see the flattened API would also serve as a good simplification, the abstract API has an advantage that it will be possible to support test fixtures. An example use case is when en application needs to load a database schema and maybe some test data before running the tests, and then remove it again after the tests have run. Another example would be a fixture which constructs a big randomized data structure on which a number of quicker incremental tests are performed. Not to solve this here, but to see how it may fit into the abstract API, I imaging the fixture to be something like a function paired with a finalizer which can be applied to a test: val fixture : setup: ('a -> 'b promise) -> ?teardown: ('b -> unit promise) -> string -> ('a, 'b) fixture
val with_fixture : ('a, 'b) fixture -> 'b test -> 'a test The string argument is for reporting. Another issue I ran into when trying to use Alcotest for Caqti, is that I have a command line option which essentially evaluates to a list of database URIs on which I want to run the whole test suite, or part of it if I also merge in tests which don't require a database connection. The abstract API would allow adding something like: val group_by : ?pos: source_code_position -> ?tags: ('a -> tag_set) -> 'a Fmt.t -> 'a test -> 'a list test This can also be solved by transforming the test suite before passing it to Alcotest if I give up the convenient predefined argument parser. |
3f8ed13
to
43e5ac6
Compare
This is a proof-of-concept for a new API for test registration in which tests can be nested arbitrarily with one-or-more levels of hierarchy, as described in #275. Currently, this looks as follows:
... where the type
'a test
is abstract, unlike the current one. Using these combinators, it's possible to build test suites with arbitrary tree structures:Another benefit of this more abstract API is that it's extensible to different classes of test, such as the snapshot testing supported by e.g. Jest. This PR is still quite scrappy but demonstrates the following high-level features that I'd like to provide:
Test metadata
(Related issues: #124, #293.)
Firstly, the tests (and test groups) can now have attached source positions via
~here
, allowing Alcotest to point to specific failing tests. Secondly, it's now possible to attach user-defined metadata to tests via~tags
. These are arbitrary typed values, similar in design toLogs.Tag
. The main purpose of tags would be to allow running user-defined test filters at runtime, using something similar to:(In reality I'm also proposing we hide the various config parameters to
run
inside aConfig
abstraction to stop them polluting the API as they do now.) This would supersede the existing[ `Quick | `Slow ]
distinction, and also enable ways to conditionally disable tests (e.g.Sys.os_type
,Sys.word_size
, environment variables).Another possible use of this metadata (that I haven't explored yet) would be to provide editor integration that allows running a selected / highlighted test (or set of tests) directly from the editor. I suspect that this
tags
mechanism could also handle attaching locks to tests in order to support a concurrent test runner (related issue: #177).PPX-friendly test registration
(Related issues: #126, #256.)
A nice property of having a flexible structure for test suites is that it's simple to implement a PPX that directly translates code structure into Alcotest registration boilerplate. I think it's important that Alcotest have a clean API without requiring a PPX, so this could be considered a follow-up feature, but as a proof of concept this PR contains the shell of such a PPX.
The gist is that the following code:
can be executed to give the following result:
This PPX can also support: (a) suites defined over multiple files, and (b) test suites that instantiate functors.
We could provide some other PPX features for making test assertions with proper location information but this is unrelated. (It should also be possible to attach tags to such tests; I haven't looked into this yet.)
Backwards compatibility
As proposed, this PR is a substantial breaking change to the Alcotest API. For the reasons described above, I think it's worthwhile to make this change, but fortunately it's possible to ease the pain of transition quite a bit. The old API can be implemented entirely in terms of the old one, so it's possible to upgrade the core test runner and its API separately.
I propose a migration path like the following:
upgrade the core of the test runner to support the V2 API, but keep the same top-level combinators for the V1 API;
alias the V1 API under
Alcotest.V1
(which can be supported in that form even after the V2 switch), and provide the new API under something likeV2_alpha
with an instability warning;provide a PPX for generating the registration boilerplate, which would use the V2 API inside its own namespace. (The hope being that users adopt the PPX and so make the V1 → V2 transition completely silent for them);
release an Alcotest 2.0 with the default API upgraded and the old one still provided under
Alcotest.V1
. For users who don't want to upgrade their registration boilerplate and don't want to use the PPX, they could addmodule Alcotest = Alcotest.V1
in their headers to run on 2.0.It's possible to imagine other migration paths, for instance taking the Ounit2 route and providing a new top-level module, or having the new combinators sit inside the same API as the old one. Having considered it a bit, I decided that a more opinionated line is probably warranted provided we can provide a sensible opt-out.