Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# - How to properly implement Setup and initialize variables with NUnit

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.

like image 789
David Jiménez Martínez Avatar asked Sep 08 '15 13:09

David Jiménez Martínez


1 Answers

Let's reduce the problem to a more manageable size first:

Reduced problem

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.

Functions that return values

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#.

like image 177
Mark Seemann Avatar answered Nov 17 '22 00:11

Mark Seemann