Can a particular constructor argument of an injected SUT using Autodata xUnit Theories?
Asked Answered
P

1

3

Consider the following test,

[Theory, MyConventions]
public void GetClientExtensionReturnsCorrectValue(BuilderStrategy sut)
{
    var expected = ""; // <--??? the value injected into BuilderStrategy
    var actual = sut.GetClientExtension();
    Assert.Equal(expected, actual);
}

and the custom attribute I'm using:

public class MyConventionsAttribute : AutoDataAttribute {
    public MyConventionsAttribute()
        : base(new Fixture().Customize(new AutoMoqCustomization())) {}
}

and the SUT:

class BuilderStrategy {
    private readonly string _clientID;
    private readonly IDependency _dependency;
    public void BuilderStrategy(string clientID, IDependency dependency) {
        _clientID = clientID;
        _dependency = dependency;      
    }
    public string GetClientExtension() {
        return _clientID.Substring(_clientID.LastIndexOf("-") + 1);
    }
}

I need to know what value was injected into the constructor parameter clientID so that I can use it to compare with the output of GetClientExtension. Is it possible to do this while still writing this style of test where the SUT is injected into the test method?

Plataea answered 14/8, 2013 at 17:27 Comment(0)
C
6

If you expose the injected clientID (and dependency as well) as read-only properties, you can always query their values:

public class BuilderStrategy {
    private readonly string _clientID;
    private readonly IDependency _dependency;
    public void BuilderStrategy(string clientID, IDependency dependency) {
        _clientID = clientID;
        _dependency = dependency;      
    }
    public string GetClientExtension() {
        return _clientID.Substring(_clientID.LastIndexOf("-") + 1);
    }

    public string ClientID
    {
        get { return _clientID; }
    }

    public IDependency Dependency
    {
        get { return _dependency; }
    }
}

This doesn't break encapsulation, but is rather known as Structural Inspection.

With this change, you could now rewrite the test like this:

[Theory, MyConventions]
public void GetClientExtensionReturnsCorrectValue(BuilderStrategy sut)
{
    var expected = sut.ClientID.Substring(sut.ClientID.LastIndexOf("-") + 1);
    var actual = sut.GetClientExtension();
    Assert.Equal(expected, actual);
}

Some people don't like duplicating production code in the unit test, but I would rather argue that if you follow Test-Driven Development, it's the production code that duplicates the test code.

In any case, this is a technique known as Derived Value. In my opinion, as long as it retains a cyclomatic complexity of 1, we can still trust the test. Additionally, as long as the duplicated code only appears in two places, the rule of three suggests that we should keep it like that.

Concomitant answered 14/8, 2013 at 17:55 Comment(7)
Thanks, Mark, also for the embedded references. In using Structural Inspection, should the readonly properties be part of the interface or only the implementations under test (assuming there exists an IBuilderStrategy which BuilderStrategy implements)? Also, I would think that it would be better to expose these properties as the need arises in unit testing, rather than expose all dependencies as a routine matter (although, I suppose that in practicing TDD, this would all happen naturally). Do you have an opinion on any of that?Plataea
Using Derived Value it seems possible to get False Negative results if your tests are written in C# and your SUT is written in VB.NET. In my code, R# suggested using StringComparison.Orginal with LastIndexOf in the C# test, but not in the VB.NET-written SUT. The R# suggestion isn't the problem, but it indicates that using Derived Value either tightly couples you to a particular .NET language, or expose you to sneaky differences that exist between languagesPlataea
The property is part of the concrete class: "Ultimately, adding a property to a concrete class has no impact on the exposed interface."Concomitant
I'm not sure I follow the logic about False Negatives... If you follow TDD and the Red/Green/Refactor cycle, you should see the test fail before you make it pass. That provides good protection against False Negatives. Ultimately, the test is the specification; if you end up writing a different implementation in your production code, it doesn't really matter as long as it matches the specification.Concomitant
Point taken re: TDD & Red/Green/Refactor. Sadly, I'm having to reverse-engineer this on existing code ;) So, in creating the Derived Value, it is only important that the code used to create it is equivalent to that of the implementation, not that it is an exact copy (allowing for different .NET languages to be used). The burden is then shifted (rightly so) to the tests to account for any bizarre edge cases you can think of. Testing specific values could then be done with a different test using inline data, when random values for (ClientID, in this example) are not explicit enough.Plataea
It sounds like you're writing Characterization Tests: agileinaflash.blogspot.dk/2009/02/…Concomitant
You're right - thanks for another link to a great resource I'd not seen before!Plataea

© 2022 - 2024 — McMap. All rights reserved.