3

I have a class that I'm trying to get under unit tests. The class exposes a struct as a public property. The struct also has some public methods (that do much more than a method in a struct should). I can't make changes to the struct (I don't own that code and it would be too risky at the moment).

Mock doesn't work on value types. Is there a way to effectively "mock a struct"?

public class SystemUnderTest
{
    public FragileStructCantChange myStruct {get; set;}

    public string MethodUnderTest()
    {
        if(myStruct.LongRunningMethod() == "successful")
            return "Ok";
        else
            return "Fail";        
    }
}

public struct FragileStructCantChange
{
    public string LongRunningMethod()
    {
        var resultString = DoStuff(); //calls other subsystems            
        return resultString;
    }
}
Ben Joiner
  • 33
  • 1
  • 4
  • You said you can't change it but does that include implementing an interface? – Crowcoder Feb 15 '19 at 23:09
  • Could you share `DoStuff` method? – Johnny Feb 15 '19 at 23:53
  • @Crowcoder, I might be able to add an interface. This is the way I was leaning. If you write it as an answer I'll mark it as accepted. – Ben Joiner Feb 16 '19 at 00:15
  • @Johnny, DoStuff() is really a bunch of calls. I'm trying really hard to avoid touching LongRunningMethod() at this point. – Ben Joiner Feb 16 '19 at 00:15
  • @BenJoiner not sure it can be answered if you do not share `DoStuff`. Actually here the whole point is to mock the calls within `DoStuff`... – Johnny Feb 16 '19 at 16:30

1 Answers1

2

Referencing a struct through an interface effectively turns it into a reference type, through a process called "boxing". Boxing a struct can cause some subtle changes in behavior, which you should read about before making your decision.

Another option is to add some indirection between the struct and the code under test. One way is to add a to specify a test value.

public class SystemUnderTest
{
    public FragileStructCantChange myStruct { get; set; }

    // This value can be overridden for testing purposes
    public string LongRunningMethodOverride { get; set; }

    public string MethodUnderTest()
    {
        // Call the indirect Func instead of referencing the struct directly
        if (LongRunningMethod() == "successful")
            return "Ok";
        else
            return "Fail";
    }

    private string LongRunningMethod()
        => LongRunningMethodOverride ?? myStruct.LongRunningMethod();
}

public class Tests
{

    [Fact]
    public void TestingSideDoor()
    {
        var sut = new SystemUnderTest();

        // Override the func to supply any test data you want
        sut.LongRunningMethodOverride = "successful";

        Assert.Equal("Ok", sut.MethodUnderTest());
    }
}

Another way is to expose an overridable function pointer...

public class SystemUnderTest
{
    public FragileStructCantChange myStruct { get; set; }

    // This Func can be overridden for testing purposes
    public Func<string> LongRunningMethod;

    public SystemUnderTest() {
        LongRunningMethod = () => myStruct.LongRunningMethod();
    }

    public string MethodUnderTest()
    {
        // Call the indirect Func instead of referencing the struct directly
        if (LongRunningMethod() == "successful")
            return "Ok";
        else
            return "Fail";
    }
}

public class Tests
{

    [Fact]
    public void TestingSideDoor()
    {
        var sut = new SystemUnderTest();

        // Override the func to supply any test data you want
        sut.LongRunningMethod = () => "successful";

        Assert.Equal("Ok", sut.MethodUnderTest());
    }
}

Another option is to use a virtual method, which can be faked either by a subclass or a mocking framework (Moq, in this example)...


public class SystemUnderTest
{
    public FragileStructCantChange myStruct { get; set; }

    // This method can be overridden for testing purposes
    public virtual string LongRunningMethod()
        => myStruct.LongRunningMethod();

    public string MethodUnderTest()
    {
        // Call the indirect method instead of referencing the struct directly
        if (LongRunningMethod() == "successful")
            return "Ok";
        else
            return "Fail";
    }
}

public class Tests
{

    [Fact]
    public void TestingSideDoor()
    {
        var sut = new Mock<SystemUnderTest>();

        // Override the method to supply any test data you want
        sut.Setup(m => m.LongRunningMethod())
            .Returns("successful");

        Assert.Equal("Ok", sut.Object.MethodUnderTest());
    }
}

I won't claim that either of these options is pretty, and I'd think hard about safety/security before putting this sort of backdoor into a shared library. An interface is usually preferable, when viable.

This sort of thing works, though, and I think it can be a fair compromise when you're faced with test-unfriendly code that you don't want to invasively refactor.

xander
  • 1,689
  • 10
  • 18
  • hm... it is strange to mock `SystemUnderTest` – Johnny Feb 16 '19 at 20:03
  • Yeah, it's strange--no argument from me. It's a "last resort" way to add a bit of testability to something that's otherwise untestable. Sometimes, you just need a foot in the door to gain some confidence before refactoring. Sometimes, you're stuck with code structure that you can't change. Either way, I'd take "strange" over "no tests at all." – xander Feb 16 '19 at 20:26
  • I've updated my answer to include a third option. I't simpler, and probably a bit less polarizing. – xander Feb 16 '19 at 20:27
  • @xander. Great point about the boxing, I didn't even think about that. I also agree that it's weird to mock the SUT. ' – Ben Joiner Feb 18 '19 at 17:32
  • Thanks to all three of you for some great options! – Ben Joiner Feb 18 '19 at 17:34
  • Usually when you try to test already existing(legacy) code, you mock nothing - you build integration tests around, and after that you can refactor and unit test it with confidence. – Fabio Mar 05 '19 at 00:47
  • Agreed; it's better to add integration tests (specifically, ["pinning tests"](http://wiki.c2.com/?PinningTests) and then refactor for testability, when feasible. In this case, some of the code is out of the author's control, and can't be refactored. In other cases, we might not have the resources to build a proper pinning test (maybe there are time constraints, or a limited-functionality dev environment, or simply not enough will to do a full refactor). While not ideal, I think this sort of mocking is a fair compromise. Every little trick helps when working with legacy code! – xander Mar 05 '19 at 01:13