In F# we typically organize tests much like in C# or VB.NET: writing functions marked with a
[<Test>] attribute or similar. Actually there's a slight advantage in F#: you don't need to write a class marked as test fixture, you can directly write the tests as let-bound functions. Still, it's fundamentally the same model. (If you're into BDD there's also TickSpec as an alternative model).
Since it's the same model, you get the same issues I described in my last post, and then some: for example as Kurt explains, attributes in F# sometimes aren't treated exactly as in C#.
Also in my last post, I wrote about how MbUnit supports first-class tests as an alternative to attribute-defined tests. In F# we can take advantage of this and custom operators to build a very concise DSL to define tests.
First let's see a small test suite with setup/teardown, written with the classic attributes:
[<TestFixture>] type ``MemoryStream tests``() = let mutable ms : MemoryStream = null [<SetUp>] member x.Setup() = ms <- new MemoryStream() [<TearDown>] member x.Teardown() = ms.Dispose() [<Test>] member x.``Can read``() = Assert.IsTrue ms.CanRead [<Test>] member x.``Can write``() = Assert.IsTrue ms.CanWrite
Looks simple enough, right? And yet, the mutable field is a smell, or at least an indicator that this isn't functional. Let's try to get rid of that mutable.
As a first step we'll rewrite this as first-class tests, that is, using
[<StaticTestFactory>] as shown in my last post:
[<StaticTestFactory>] let testFactory() = let suite = TestSuite("MemoryStream tests") let ms : MemoryStream ref = ref null suite.SetUp <- fun () -> ms := new MemoryStream() suite.TearDown <- fun () -> (!ms).Dispose() let tests = [ TestCase("Can read", fun () -> Assert.IsTrue (!ms).CanRead) TestCase("Can write", fun () -> Assert.IsTrue (!ms).CanWrite) ] Seq.iter suite.Children.Add tests [suite]
Oh great, that's even uglier than what we started with! And we have replaced the mutable field with a ref cell, not much of an improvement.
But bear with me, we have first-class tests now so there's a lot a room for improvement.
In order to keep refactoring this, we need to realize that the problem is that our test cases should be functions
MemoryStream -> unit instead of
unit -> unit. That way, they wouldn't have to depend on an external
MemoryStream instance; instead the instance would be pushed somehow to the test. Let's write that:
let tests = [ "Can read", (fun (ms: MemoryStream) -> Assert.IsTrue ms.CanRead) "Can write", (fun ms -> Assert.IsTrue ms.CanWrite) ]
Now we have this list of strings and
MemoryStream -> unit functions. What we need now is to turn these functions into
unit -> unit so we can ultimately build TestCases.
In other words, we need a function
(MemoryStream -> unit) -> (unit -> unit). This function should create the
MemoryStream, pass it to our test function, then dispose the
MemoryStream. Hey, what do you know, turns out that's just what SetUp and TearDown do!
Still with me? It's much easier to see this in code:
let withMemoryStream f () = use ms = new MemoryStream() f ms
Now we apply this to our list, building the TestCases and then the TestSuite:
[<StaticTestFactory>] let testFactory() = let suite = TestSuite("MemoryStream tests") tests |> Seq.map (fun (n,t) -> TestCase(n, Gallio.Common.Action(withMemoryStream t))) |> Seq.iter suite.Children.Add [suite]
We've eliminated all mutable references, and also replaced SetUp/TearDown with a simple higher-order function.
But we can do still better, in terms of readability. We can define a few custom operators to hide the TestSuite and TestCase constructors:
let inline (=>>) name tests = let suite = TestSuite(name) Seq.iter suite.Children.Add tests suite :> Test let inline (=>) name (test: unit -> unit) = TestCase(name, Gallio.Common.Action test) :> Test [<StaticTestFactory>] let testFactory() = [ "MemoryStream tests" =>> [ "Can read" => withMemoryStream(fun ms -> Assert.IsTrue ms.CanRead) "Can write" => withMemoryStream(fun ms -> Assert.IsTrue ms.CanWrite) ] ]
And with a couple more operators we get rid of the duplicate call to
let inline (+>) f = Seq.map (fun (name, partialTest) -> name => f partialTest) let inline (==>) (name: string) test = name,test [<StaticTestFactory>] let testFactory() = [ "MemoryStream tests" =>> withMemoryStream +> [ "Can read" ==> fun ms -> Assert.IsTrue ms.CanRead "Can write" ==> fun ms -> Assert.IsTrue ms.CanWrite ] ]
Confused about all those kinds of arrows? The good thing about first-class tests is that you can build them any way you want, no need to use these operators if you don't like them. That's also precisely one of its downsides: as there is no fixed idiom, it can get harder to read compared to attribute-based test definitions, where there is a single, well-defined way to do things.
In my last post I showed how first-class tests practically eliminate the concept of parameterized tests. In this post I showed how they eliminate the concept of setup/teardown, replacing them with a higher-order function, a more generic concept.
More generally, I'd say that whatever domain you're modeling (in this case, tests), there is much to gain if the core concepts are representable as first-class values. It should also be noted that different languages have very different notions of what language objects are first-class values. Some are more flexible than others, but that doesn't imply any superiority by itself. However it does mean that if you're not aware of this you'll probably misuse your language and end up with ever more complex workarounds to manipulate your domain objects as values. Nice APIs, conventions, configuration, etc, are all secondary and can be built much more easily on top of composable, first-class building blocks.
But I digress. In the next post I'll show a simple testing library built around tests as first-class values and more pros/cons about this approach.