So I’ve thought on it a little more, and I think I have a better intuition on what makes Haskell’s testing situation a little unique from other languages.
Most languages use a OO method-orientated design for test layout and discovery. For example: Java uses the @Test
annotation to indicate a test; Python’s unittest framework takes any class extending unittest.TestCase
and assumes that all its functions are test cases.
Haskell differs in a few ways:
- the organization of test code is handled by an eDSL where you create a value representing a test tree, typically with combinators such as
testGroup
or testCase
, and assigning them names
- we do not have access to location data for a test case, whereas Java and the like have more immediate access to this data. This is because combinators such as
testGroup
do not typically have access to the location they were called at.
- this one is more conceptual: Haskell test structures are less focused around top level declarations in comparison to other languages. In other languages, it is not as easy to have nested test cases within one declaration. In Haskell a single top-level declaration can quite easily contain many different tests.
In other words, in Haskell, the unit of test is actually a testGroup
or testCase
call, and not top level declarations. The test tree is normally discovered (e.g. tasty
and HUnit
) with a flag such as --list-tests
.
I am wondering whether it is possible to have test frameworks provide not just the test tree, but also the location of said tests. For example:
-- Test.hs
tests = testGroup "all" [testGroup "unit" unitTests, testGroup "property" propertyTests]
--Test.Unit.hs
unitTests = [testCase "my unit" ..., testCase "unit2" ...]
-- Current output of --list-tests
all.unit.unit1
all.unit.unit2
all.property.prop1
-- Suggested output of --list-tests-json
{"name" : "all.unit.my unit", "location" : "path/to/Test/Unit.hs"...}
I think this is a preferable option for a few reasons:
- We can run compiled code for tests, not interpreted, which would be the case with a GHCi solution
- It is what other test experiences appear to do
- This solution provides a very easy way to get the global test tree (see screenshot below from a sample python project)
- This is, afaict, what IntelliJ and other IDEs do for their test integration. For example, running test1 in the image below incurs the following command line call:
/.../python3.9 ~/.vscode/extensions/.../visualstudio_py_testlauncher.py --us=. --up=*test.py --uvInt=2 --result-port=53890 -ttest.MyTest.test2 --testFile=./test.py
I think the type class oriented design addresses a use case that I also think is convincing: what if I have a function that could be run, such as an IO ()
or a function whose arguments have Arbitrary
instances. In some ways I think the two use-cases are very related, but I think the experience provided by most IDEs would not work with a type class solution for the reasons above…