Skip to content

Instantly share code, notes, and snippets.

@jcansdale
Last active January 24, 2020 19:56
Show Gist options
  • Save jcansdale/de58c2b2c389851f72b00ef525be820c to your computer and use it in GitHub Desktop.
Save jcansdale/de58c2b2c389851f72b00ef525be820c to your computer and use it in GitHub Desktop.
Unit testing challenge

I'm looking for the cleanest way to make the test below pass.

The rules are:

  1. It must be a unit test (so don't touch the file system).
  2. The public signature for CodeUnderTest must not be changed. can be extended not reduced
  3. You're allowed to use DI/your favorite mocking framework.
  4. You can only add code where you see /**/.
        [Test]
        public void Test()
        {
            /**/
            var text = CodeUnderTest.GetUpperText("foo.txt" /**/);

            Assert.That(text, Is.EqualTo("BAR"));
            /**/
        }

        public class CodeUnderTest
        {
            /**/
            public static string GetUpperText(string path /**/)
            {
                var text = File.ReadAllText(path);
                return text.ToUpperInvariant();
            }
            /**/
        }

Please comment below or tweet me @jcansdale.

@ploeh
Copy link

ploeh commented Sep 26, 2016

A good functional implementation of this challenge is trivial, so here we suffer from the simplicity of the original problem statement. While you can call File.ReadAllText from an F# function, it's by definition impure. The property of impurity is transitive, so because File.ReadAllText is impure, any function calling it would also be impure.

In functional programming, we really, really like our functions to be pure, so we'd refactor any impure indirect input to direct input. Since the output type of File.ReadAllText is string, this can be refactored into a function that takes a string as input:

let getUpperText (text : string) = text.ToUpperInvariant ()

This is where the simplicity of the example begins to show, because the getUpperText function doesn't seem to add much value... Again, we should consider it a stand-in for more complex logic.

Since this function is pure, it's easy to unit test:

[<Theory>]
[<InlineData("foo", "FOO")>]
[<InlineData("bar", "BAR")>]
let ``getUpperText returns correct value`` input expected =
    let actual = getUpperText input
    Assert.Equal(expected, actual)

In a production system, then, you can compose it with any source of string values, including the contents of files:

// string -> string
File.ReadAllText >> getUpperText

That example composes a function that takes a string (a file path) as input, and returns a string.

For a more complex demonstration of such separation of pure and impure functions, see e.g. this article: http://blog.ploeh.dk/2016/03/18/functional-architecture-is-ports-and-adapters

@jcansdale
Copy link
Author

This is where the simplicity of the example begins to show, because the getUpperText function doesn't seem to add much value... Again, we should consider it a stand-in for more complex logic.

I wanted a simple stand-in that wouldn't obscure what was going on. Looks like I made it a little too simple!

How about we up the complexity slightly to this:

    public static string GetUpperText(string path)
    {
        if(!File.Exists(path)) return "DEFAULT";
        var text = File.ReadAllText(path);
        return text.ToUpperInvariant();
    }

Testing it using StaticMocks would look like this (expand)

using StaticMocks; // Find it on NuGet.
using NSubstitute;
using NUnit.Framework;
using System;
using System.IO;

public class Tests
{
    [Test]
    public void GetUpperText_FromFile()
    {
        using (var staticMock = new StaticMock(typeof(CodeUnderTest)))
        {
            staticMock.For(() => File.Exists(null)).ReturnsForAnyArgs(false);
            staticMock.For(() => File.Exists("foo.txt")).Returns(true);
            staticMock.For(() => File.ReadAllText("foo.txt")).Returns("bar");

            var text = CodeUnderTest.GetUpperText("foo.txt");

            Assert.That(text, Is.EqualTo("BAR"));
        }
    }

    [Test]
    public void GetUpperText_Default()
    {
        using (var staticMock = new StaticMock(typeof(CodeUnderTest)))
        {
            staticMock.For(() => File.Exists(null)).ReturnsForAnyArgs(false);
            staticMock.For(() => File.ReadAllText(null)).Returns(args =>
            {
                throw new FileNotFoundException((string)args[0]);
            });

            var text = CodeUnderTest.GetUpperText("foo.txt");

            Assert.That(text, Is.EqualTo("DEFAULT"));
        }
    }
}

public class CodeUnderTest
{
    public static string GetUpperText(string path)
    {
        if(!File.Exists(path)) return "DEFAULT";
        var text = File.ReadAllText(path);
        return text.ToUpperInvariant();
    }

    class File
    {
        internal static Func<string, string> ReadAllText = (string path) => System.IO.File.ReadAllText(path);
        internal static Func<string, bool> Exists = (string path) => System.IO.File.Exists(path);
    }
}

How would testing that look in F#?

Am I correct in thinking all top level F# functions are public? Is it considered good practice to have your pure functions public and hence accessible for testing? If I was writing C# in a more functional style, wouldn't many of these functions tend to be private?

Thanks for the links. This is all very interesting. 😄

@PMBjornerud
Copy link

Looks like a method containing both file system operations and business logic. I'd split them.

One method (impure) for getting the file content, or null if no file.
One method (pure) containing the rules for how to format the result.

These methods would be standalone and you would only have to change one of them if the data layer or business rules changed. Integration tests for the first. Unit tests for the second (which often contains the edge cases and complex logic).

I remember one article that described an idea as "dependency elimination", which I guess this relates to.
http://qualityisspeed.blogspot.no/2014/09/beyond-solid-dependency-elimination.html

(And since this is the internet, obligatory troll content: "Mocking considered harmful" ;)

@ploeh
Copy link

ploeh commented Sep 26, 2016

How would that look in F#? That question triggered a blog post, but the summary is this:

// string -> string
let getUpperText path =
    path
    |> Some
    |> Option.filter File.Exists
    |> Option.map (File.ReadAllText >> getUpper)
    |> Option.defaultIfNone "DEFAULT"

Apart from getUpper there's nothing else to test 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment