Xunit test standards and Nsubstitute approaches

Naming conventions

MethodName_Expectedbehaviour_conditions

https://dzone.com/articles/7-popular-unit-test-naming

Parameterized unit test

https://andrewlock.net/creating-parameterised-tests-in-xunit-with-inlinedata-classdata-and-memberdata/

xUnit uses the [Fact] attribute to denote a parameterless unit test, which tests invariants in your code.

In contrast, the [Theory] attribute denotes a parameterised test that is true for a subset of data. That data can be supplied in a number of ways, but the most common is with an [InlineData] attribute.

[Theory]
[InlineData(1, 2, 3)]
[InlineData(-4, -6, -10)]
[InlineData(int.MinValue, -1, int.MaxValue)]
public void CanAddTheory(int value1, int value2, int expected)

 

If the values you need to pass to your [Theory] test aren’t constants, then you can use an alternative attribute, [ClassData], to provide the parameters. This attribute takes a Type which xUnit will use to obtain the data:

[Theory]
[ClassData(typeof(CalculatorTestData))]
public void CanAddTheoryClassData(int value1, int value2, int expected)
public class CalculatorTestData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 1, 2, 3 };
        yield return new object[] { -4, -6, -10 };
        yield return new object[] { -2, 2, 0 };
        yield return new object[] { int.MinValue, -1, int.MaxValue };
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

 

The [MemberData] attribute can be used to fetch data for a [Theory] from a static property or method of a type. This attribute has quite a lot options, 

The [MemberData] attribute can load data from an IEnnumerable<object[]> property on the test class. 

    [Theory]
    [MemberData(nameof(Data))]
    public void CanAddTheoryMemberDataProperty(int value1, int value2, int expected)

    public static IEnumerable<object[]> Data =>
        new List<object[]>
        {
            new object[] { 1, 2, 3 },
            new object[] { -4, -6, -10 },
            new object[] { -2, 2, 0 },
            new object[] { int.MinValue, -1, int.MaxValue },
        };
    public static IEnumerable<object[]> GetData(int numTests)
    {
        var allData = new List<object[]>
        {
            new object[] { 1, 2, 3 },
            new object[] { -4, -6, -10 },
            new object[] { -2, 2, 0 },
            new object[] { int.MinValue, -1, int.MaxValue },
        };

        return allData.Take(numTests);
    }



Throw exceptions

Callbacks can be used to throw exceptions when a member is called.

//For non-voids:
calculator.Add(-1, -1).Returns(x => { throw new Exception(); });
 
//For voids and non-voids:
calculator
    .When(x => x.Add(-2, -2))
    .Do(x => { throw new Exception(); });

 

 

Assert received method calls, properties, indexer, event subscriptions, event invocation

http://nsubstitute.github.io/help/received-calls/

var command = Substitute.For<ICommand>();
 
//Check received with second arg of 2 and any first arg:
calculator.Received().Add(Arg.Any<int>(), 2);
 
 
 
//Check did not receive a call where second arg is >= 500 and any first arg:
calculator
.DidNotReceive()
.Add(Arg.Any<int>(), Arg.Is<int>(x => x >= 500));
 
//Number of times
command.Received(3).Execute(); // << This will fail if 2 or 4 calls were received
 
//ignore arguments
calculator.ReceivedWithAnyArgs().Add(1,1);
 
 

Can’t mock an LogError extension method. What you should mock, is the ILogger.Log method, which LogError calls into. It makes the verification code a bit clunky, but it should work

public static class MockExtensions
{
/// <summary>
/// Checks the given logger received a message which contains given ‘ExpectedLogMessage’
/// Throws exception in case given log message is not received
//https://stackoverflow.com/questions/39604198/how-to-test-asp-net-core-built-in-ilogger
//https://www.clearlyagileinc.com/agile-blog/using-nsubstitute-to-check-if-method-called-with-particular-object
//If LogInformation() has not been received NSubstitute will throw a ReceivedCallsException
//and let you know what call was expected and with which arguments, as well as
//listing actual calls to that method and which the arguments differed.
//Log<Object>(Information, 0, *Xml File or XML folder Path is not Specified in command line arguments or configuration file.*, <null>, Func<Object, Exception, String>)
/// </summary>
/// <typeparam name=”T”></typeparam>
/// <param name=”logger”></param>
/// <param name=”ExpectedLogLevel”></param>
/// <param name=”LogExpected”></param>
/// <param name=”args”></param>
public static void AssertLog<T>(this ILogger<T> logger,
LogLevel ExpectedLogLevel, string ExpectedLogMessage, params object[] args)
{
logger.Received().Log(logLevel: ExpectedLogLevel, eventId: Arg.Any<EventId>()
, state: Arg.Is<FormattedLogValues>(data =>
data.ToString().Contains( string.Format(ExpectedLogMessage, args) ))
, exception: Arg.Any<Exception>(), formatter: Arg.Any<Func<Object, Exception, String>>());
}

}

 
 

HTTP / API mocking

  • I find the unit testing approach as broadly practiced to be way too granular and it’s not obvious that the strategy of mocking and faking and tickling every method and property singly yields higher coverage for the code you care about.
  • What we in our team generally aim for internally is not to wedge ourselves into parts of the framework our team doesn’t own (we don’t use the source code of HttpWebRequest, we use what you use) but rather create an environment in the small that can give us the answers we’re looking for to achieve maximum coverage during build-verification testing.
  • That means that if we need one, we’ll have a mock HTTP service that can run on every developer box (self-hosted in the same process, even) and spews out the right set of errors and we run that as we execute the unit tests.

Database mocking

  • If tests have a dependency on a database we’ll have a little local one that’s just capable enough and prepopulated for predictable results in order to exercise the scenario.

References

Leave a Reply

Your email address will not be published. Required fields are marked *