In my last two posts I showed how MbUnit supports first-class tests, and how you could use that to build a DSL in F# around it.
I explained how many concepts in typical xUnit frameworks can be more simply expressed when tests are first-class values, which is not the case for most .NET and Java test frameworks.
More concretely, test setup/teardown is a function over a test, and parameterized tests are... just data manipulation.
Since first-class tests greatly simplify things, why not dispense with the typical class-based, attribute-driven approach and build a test library around first-class tests? Well, Haskellers have been doing this for at least 10 years now, with HUnit.
HUnit organizes tests using this tree:
-- | The basic structure used to create an annotated tree of test cases. data Test -- | A single, independent test case composed. = TestCase Assertion -- | A set of @Test@s sharing the same level in the hierarchy. | TestList [Test] -- | A name or description for a subtree of the @Test@s. | TestLabel String Test
Where Assertion
is simply an alias for IO ()
. This is all you need to organize tests in suites and give them names.
We can trivially translate this to F# :
type TestCode = unit -> unit type Test = | TestCase of TestCode | TestList of Test seq | TestLabel of string * Test
Let's see an example:
let testA = TestLabel ("testsuite A", TestList [ TestLabel ("test A", TestCase(fun _ -> Assert.AreEqual(4, 2+2))) TestLabel ("test B", TestCase(fun _ -> Assert.AreEqual(8, 4+4))) ])
It's quite verbose, but we can define the same DSL as I defined earlier for MbUnit tests, so this becomes:
let testA = "testsuite A " =>> [ "test A" => fun _ -> Assert.AreEqual(4, 2+2) "test B" => fun _ -> Assert.AreEqual(8, 4+4) ]
Actually, I first ported HUnit (including this DSL), then discovered that MbUnit has first-class tests and later wrote the DSL around MbUnit. Everything I described in those posts (setup/teardown as higher-order functions, parameterized tests as simple data manipulation, arbitrary nesting of test suites) applies here in the exact same way.
In fact, MbUnit's class hierarchy of Test/TestSuite/TestCase can be read as the following algebraic data type:
type Test = | TestSuite of string * Test list | TestCase of string * Action
which turns out to be very similar to the tree we translated from HUnit, only the names are embedded instead of being a separate case.
I called this HUnit port Fuchu (it doesn't mean anything), it's on github.
Assertions
Fuchu doesn't include any assertion functions, or at least not yet. (EDIT: assertions were added in 0.2.0) It only gives you tools to organize and run tests, but you're free to use NUnit, MbUnit, xUnit, NHamcrest, etc, or more F#-ish solutions like Unquote or FsUnit or NaturalSpec for assertions.
Tighter integration with FsCheck is planned. (EDIT: it was added in the first release of Fuchu)
Runner/Tooling
As with HUnit, the test assembly is the runner itself. That is, as opposed to having an external test runner as with most test frameworks, your test assembly is an executable (a console application). This is because it's more of a library instead of a framework. As a consequence, there is no need of installing any external tool to run tests (just hit CTRL-F5 in Visual Studio) or debug tests (just set your breakpoints and hit F5 in Visual Studio). Here's a clear signal of why this matters:
How does one run a #fsharp unit test project in #vs11?
— Community for F# (@c4fsharp) March 23, 2012
So how do you run tests with Fuchu? Given a test suite testA like the one defined above, you can run it like this:
[<EntryPoint>] let main _ = run testA // or runParallel
But this is quite inconvenient, as it's common to split tests among different modules/files and this would mean having to list all tests somewhere, to feed them to the run function. HUnit works around this using Template Haskell, and OUnit (OCaml's port of HUnit) users generate the boilerplate code by parsing the tests' source code.
In .NET we can just decorate the tests with an attribute and then use reflection to fetch them:
[<Tests>] let testA = "2+2=4" => fun _ -> Assert.AreEqual(4, 2+2) [<Tests>] let testB = "2*3=6" => fun _ -> Assert.AreEqual(7, 2*3)
[<EntryPoint>]
let main args = defaultMainThisAssembly args
This function defaultMainThisAssembly
does exactly what it says on the tin. Notice that it also takes the command-line args, so if you call it with "/m" it will run the tests in parallel. (Curiously, you can't say let main = defaultMainThisAssembly
, it won't be recognized as the entry point).
By the way, this is just an example, you wouldn't normally annotate every single test with the Tests
attribute, only the top-level test group per module.
Run this code and you get an output like this:
2*3=6: Failed: Expected: 7 But was: 6 G:\prg\Test.fs(15,1): SomeModule.testB@15.Invoke(Unit _arg1) (00:00:00.0017681) 2 tests run: 1 passed, 0 ignored, 1 failed, 0 errored (00:00:00.0058780)
If you run this within Visual Studio with F5 you can navigate to the failing assertion by clicking on the line that looks like a mini stack trace.
REPL it!
Since running tests is as easy as saying "run test", it's also convenient sometimes to do so from the F# REPL.
Pros:
- You can directly load the source code under tests in the REPL, which cuts down compilation times.
- Easy to cherry-pick one or a few tests to run instead of running all tests (with the provided Test.filter function)
Cons:
- Having to manually load all dependencies to the tests. It may be possible to work around this using a variant of this script by Gustavo Guerra.
- If you reference the assembly under test in the REPL, fsi.exe blocks the DLLs, so you have to reset the REPL session to recompile. But if you're testing F# code, you can work around this by loading source code instead of referencing the assembly.
You can see an example of running tests from the REPL here.
Other tools
Integrating with other tools is not simple. Most tools out there seem to assume that tests are organized in classes, and each test corresponds to a method or function. This also happens with MbUnit's StaticTestFactory: for example in ReSharper or TestDriven.Net you can't single out tests. Still, they can be made to run let-bound tests (which may be a test suite), so it should be possible to have some support within this limitation.
Also, no immediate support for any continuous test runner. I checked with Greg Young, he tells me that MightyMoose/AutoTest.NET can be configured to use an arbitrary executable (with limitations). Remco Mulder, of NCrunch, suggested wrapping the test runner in a test from a known test framework as a workaround. Maybe executing the tests after compilation (with a simple AfterBuild msbuild target) is enough. I haven't looked into this yet.
Coverage tools should have no problem, it makes no difference where the executable comes from.
Build tools should have no issues either; obviously FAKE is the more direct option, but I see no problems integrating this with other build tools.
C# support
I threw in a few wrapper functions to make this library usable in C# / VB.NET. Of course, it will never be as concise as F#, but still usable. I'm not going to fully explain this (it's just boring sugar) but you can see an example here.
NUnit/MbUnit test loading
Even though it may seem very different, Fuchu is still built on xUnit concepts. And since tests are first-class values, it's very easy to map tests written with other xUnit framework to Fuchu tests. For example, building Fuchu tests from NUnit test classes takes less than 100 LoC (it's already built into Fuchu)
This lets you use Fuchu as a runner for existing tests, and to write new tests. I'm planning to use this soon in SolrNet to replace Gallio (for example, Gallio doesn't work on Mono).
There is a limitation here: Fuchu can't express TestFixtureTearDowns. It can do TestFixtureSetups (and obviously SetUp/TearDown, as explained in previous posts), but not TestFixtureTearDowns (or at least not unless you treat that test suite separately). Give it a try and see for yourself :) . Is it a real downside? I don't think so (for example, TestFixtureTearDowns make parallelization harder), but it's something to be aware of. Also I haven't looked into test inheritance yet, but it should be pretty easy to support it.
Conclusions
Does .NET really need yet another test framework? Absolutely not. The current test frameworks are "good enough" and hugely popular. But since they don't treat tests as first-class values, extending them results in more and more complexity. Consider the lifecycle of a test in a typical unit testing framework. Inheritance and multiple marker attributes make it so complex that it reminds me of the ASP.NET page lifecycle.
What I propose with Fuchu is a hopefully simpler, no-magic model. Remember KISS?