I'm doing my first dive into F# at work, and I'm moving several C# unit tests I have to F# as an exercise. Our tests are quite complex, but I relish the challenge (With setups, inheritance, teardowns and so on).
As I've been seeing, mutability should be avoided if possible, but when writing the [SetUp]
parts of the tests I can't seem to find a way to jump over mutability. Example that creates a dummy XML for a test::
[<TestFixture>]
type CaseRuleFixture() =
[<DefaultValue>] val mutable xsl : XNamespace
[<DefaultValue>] val mutable simpleStylesheet : XElement
[<DefaultValue>] val mutable testNode : XElement
[<DefaultValue>] val mutable rootNode : XElement
[<DefaultValue>] val mutable root : XElement
let CreateXsltHeader(xsl: XNamespace) =
// Build XSLT header
let styleSheetRoot =
new XElement(
xsl + "stylesheet",
new XAttribute(XName.Get "version", "1.0"),
new XAttribute(XNamespace.Xmlns + "xsl", "http://www.w3.org/1999/XSL/Transform"),
new XAttribute(XNamespace.Xmlns + "msxsl", "urn:schemas-microsoft-com:xslt"),
new XAttribute(XName.Get "exclude-result-prefixes", "msxsl"),
new XAttribute(XNamespace.Xmlns + "utils", "urn:myExtension"))
let outputNode =
new XElement(
xsl + "output",
new XAttribute(XName.Get "method", "xml"),
new XAttribute(XName.Get "indent", "yes"))
styleSheetRoot.Add outputNode
styleSheetRoot
[<SetUp>]
member this.SetUp() =
this.xsl <- XNamespace.Get "http://www.w3.org/1999/XSL/Transform"
this.simpleStylesheet <- CreateXsltHeader(this.xsl)
Directory.EnumerateFiles "Templates"
|> Seq.iter(fun filepath -> this.simpleStylesheet.Add(XElement.Parse(File.ReadAllText filepath).Elements()))
let variable =
new XElement(
this.xsl + "variable",
new XAttribute(XName.Get "name", "ROOT"),
new XAttribute(XName.Get "select", "ROOT"))
this.simpleStylesheet.Add(variable)
let rootTemplate = new XElement(this.xsl + "template", new XAttribute(XName.Get "match", "/ROOT"))
this.simpleStylesheet.Add(rootTemplate);
this.rootNode <- new XElement(XName.Get "ROOT")
rootTemplate.Add(this.rootNode);
this.root <- new XElement(XName.Get "ROOT")
this.testNode <- new XElement(XName.Get "TESTVALUE")
this.root.Add(this.testNode)
[<Test>]
member this.CaseCapitalizeEachWordTest() =
this.testNode.Value <- " text to replace ";
let replaceRule = new CaseRule();
replaceRule.Arguments <- [| "INITIALS" |];
this.rootNode.Add(
replaceRule.ApplyRule [| new XElement(this.xsl + "value-of", new XAttribute(XName.Get "select", "TESTVALUE")) |]);
let parser = new XsltParserHelper(this.simpleStylesheet);
let result = parser.ParseXslt(this.root);
let value = result.DescendantsAndSelf() |> Seq.find(fun x -> x.Name = XName.Get "ROOT")
Assert.AreEqual(" Text To Replace ", value.Value)
Those [<DefaultValue>] val mutable
to declare the variables (without initializing because that's SetUp job) and make those variables available to all the class scope, and the fact that I've basically done a 1:1 translation from what I had in C# without any apparent gaining in syntax and readability gave me the chills. Is there any way to rewrite these kind of tests and setups that looks nicer? Because all examples I've seen all over internet are simple, small and do not cover these cases.
Let's reduce the problem to a more manageable size first:
In this test, you have two mutable fields being initialized in the SetUp
method:
[<TestFixture>]
type MutableTests() =
[<DefaultValue>] val mutable foo : int
[<DefaultValue>] val mutable bar : int
[<SetUp>]
member this.SetUp () =
this.foo <- 42
this.bar <- 1337
[<Test>]
member this.TheTest () =
Assert.AreEqual(42, this.foo)
Assert.AreEqual(1337, this.bar)
Obviously, this is a stand-in for the real problem.
Instead of setting class fields, why not write functions that initialize the values that you need?
module BetterTests =
let createDefaultFoo () = 42
let createDefaultBar () = 1337
[<Test>]
let ``a test using individual creation functions`` () =
let foo = createDefaultFoo ()
let bar = createDefaultBar ()
Assert.AreEqual(42, foo)
Assert.AreEqual(1337, bar)
If you wan't all the values at once (like you have access to all fields from within a class), you can define a single function that returns all values in a tuple or record:
let createAllDefaultValues () = createDefaultFoo (), createDefaultBar ()
[<Test>]
let ``a test using a single creation function`` () =
let foo, bar = createAllDefaultValues ()
Assert.AreEqual(42, foo)
Assert.AreEqual(1337, bar)
This example uses an int * int
tuple, but it might be more readable to define a record:
type TestValues = { Foo : int; Bar : int }
let createDefaultTestValues () = {
Foo = createDefaultFoo ()
Bar = createDefaultBar () }
[<Test>]
let ``a test using a single creation function that returns a record`` () =
let defaultValues = createDefaultTestValues ()
Assert.AreEqual(42, defaultValues.Foo)
Assert.AreEqual(1337, defaultValues.Bar)
Notice that, unlike classes in C#, records in F# are super-lightweight to declare.
If you want to learn more about idiomatic unit testing with F#, a good place to start could be my Pluralsight course about unit testing with F#.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With