How to mock a method with an out parameter?

SeanKilleen picture SeanKilleen · Oct 2, 2013 · Viewed 9.2k times · Source

I am using a library that uses out parameters in a function and I need to test my code using that function.

So, attempting to have mocks come to my rescue here, via Moq which I've been using in the rest of the project.

Question

I know there's a wall of text below, so the question (in advance) is:

  • Per the lead below: Can Moq mock an item with a constructor that requires parameters that isn't normally called on its own?
  • Is this a problem with my test code? With the library? With the validation library?
  • Am I utilizing Moq with out parameters?
  • Where do I even start debugging this?

Update: Leads so Far

I'm thinking this is an issue on the mocking side with mocking the IXLRow interface. Normally it appears an XLRow is only instantiated from a workbook and never through new XLRow() -- is that a factor?

The following test passes when (note: mocks):

   [Fact]
    public void TryGetValueCanReturnTrueForVieldWithAnInteger_WhenAccessingFromRow()
    {
        var workbook = new XLWorkbook();
        workbook.Worksheets.Add("TestWS");
        var wb = workbook.Worksheet("TestWS");
        wb.Cell("A1").Value = "12345";

        // NOTE: Here we're referring to the row as part of an instantiated  
        //       workbook instead of Mocking it by itself
        int output;
        Assert.True(wb.Row(1).Cell("A").TryGetValue(out output));
    }

The Code

Snippet of the method that gets a mock of a valid object():

// ...other code that sets up other parts of the row correctly
int isAnyInt = 0; //I don't care about this value, only the true/false

// set this to false to true to mimic a row being a legitimate integer
mock.Setup(m => m.Cell("B").TryGetValue(out isAnyInt)).Returns(true);

xUnit test that tests the happy path -- Gets a mock of a valid row and then ensures it passes validation. NOTE: This test passes.

    [Fact]
    public void Validate_GivenValidRow_ReturnsValid()
    {
        var mockRow = TestHelper.GetMockValidInvoiceDetailsWorksheetRow();

        var validationResult = new InvoiceDetailsWorksheetRowValidator().Validate(mockRow.Object);
        Assert.True(validationResult.IsValid);
    }

an xUnit test (basically, "does the validator fail with a cell that isn't an integer?") NOTE: This test passes.

    [Fact]
    public void Validate_GivenNonNumericClaimantID_ReturnsInvalid()
    {
        int outint = 0;

        // Get a mock of a valid row
        var mockRow = TestHelper.GetMockValidInvoiceDetailsWorksheetRow();

        // change the TryGetValue result to false
        mockRow.Setup(m => m.Cell("B").TryGetValue(out outint)).Returns(false);

        var validationResult = new InvoiceDetailsWorksheetRowValidator().Validate(mockRow.Object);
        Assert.False(validationResult.IsValid);
        Assert.Equal("ClaimantID column value is not a number.", validationResult.Errors.First().ErrorMessage);
    }

The validator (using FluentValidation):

public class InvoiceDetailsWorksheetRowValidator : AbstractValidator<IXLRow>
{
    public InvoiceDetailsWorksheetRowValidator()
    {
        RuleFor(x => x.Cell("B"))
            .Must(BeAnInt).WithMessage("ClaimantID column value is not a number.")
            .OverridePropertyName("ClaimantIDColumn");

    }

    private bool BeAnInt(IXLCell cellToCheck)
    {
        int result;
        var successful = cellToCheck.TryGetValue(out result);
        return successful;
    }
}

For reference, the method from the library:

    public Boolean TryGetValue<T>(out T value)
    {
        var currValue = Value;

        if (currValue == null)
        {
            value = default(T);
            return true;
        }

        bool b;
        if (TryGetTimeSpanValue(out value, currValue, out b)) return b;

        if (TryGetRichStringValue(out value)) return true;

        if (TryGetStringValue(out value, currValue)) return true;

        var strValue = currValue.ToString();
        if (typeof(T) == typeof(bool)) return TryGetBasicValue<T, bool>(out value, strValue, bool.TryParse);
        if (typeof(T) == typeof(sbyte)) return TryGetBasicValue<T, sbyte>(out value, strValue, sbyte.TryParse);
        if (typeof(T) == typeof(byte)) return TryGetBasicValue<T, byte>(out value, strValue, byte.TryParse);
        if (typeof(T) == typeof(short)) return TryGetBasicValue<T, short>(out value, strValue, short.TryParse);
        if (typeof(T) == typeof(ushort)) return TryGetBasicValue<T, ushort>(out value, strValue, ushort.TryParse);
        if (typeof(T) == typeof(int)) return TryGetBasicValue<T, int>(out value, strValue, int.TryParse);
        if (typeof(T) == typeof(uint)) return TryGetBasicValue<T, uint>(out value, strValue, uint.TryParse);
        if (typeof(T) == typeof(long)) return TryGetBasicValue<T, long>(out value, strValue, long.TryParse);
        if (typeof(T) == typeof(ulong)) return TryGetBasicValue<T, ulong>(out value, strValue, ulong.TryParse);
        if (typeof(T) == typeof(float)) return TryGetBasicValue<T, float>(out value, strValue, float.TryParse);
        if (typeof(T) == typeof(double)) return TryGetBasicValue<T, double>(out value, strValue, double.TryParse);
        if (typeof(T) == typeof(decimal)) return TryGetBasicValue<T, decimal>(out value, strValue, decimal.TryParse);

        if (typeof(T) == typeof(XLHyperlink))
        {
            XLHyperlink tmp = GetHyperlink();
            if (tmp != null)
            {
                value = (T)Convert.ChangeType(tmp, typeof(T));
                return true;
            }

            value = default(T);
            return false;
        }

        try
        {
            value = (T)Convert.ChangeType(currValue, typeof(T));
            return true;
        }
        catch
        {
            value = default(T);
            return false;
        }
    }

The Problem

The first test passes. But when I run this test, it fails:

   [Fact]
   public void Validate_GivenNonNumericInvoiceNumber_ReturnsInvalid()
    {
        int outint = 0; // I don't care about this value

        // Get a mock of a valid worksheet row
        var mockRow = TestHelper.GetMockValidInvoiceDetailsWorksheetRow();

        mockRow.Setup(m => m.Cell("E").TryGetValue(out outint)).Returns(false);

        // Validates & asserts
        var validationResult = new InvoiceDetailsWorksheetRowValidator().Validate(mockRow.Object);
        Assert.False(validationResult.IsValid);

        // Placed here to ensure it's the only error message. This is where it fails.
        Assert.Equal("InvoiceNumber column value is not a number.",validationResult.Errors.First().ErrorMessage);
    }

But it doesn't fail because the validation hasn't been implemented -- it fails because the other item is invalid first, even though I'm returning it from getting a valid mock -- the same valid mock that passes tests otherwise.

The message, exactly, is:

Assert.Equal() Failure

Position: First difference is at position 0

Expected: InvoiceNumber column value is not a number.

Actual: ClaimantID column value is not a number.

I would expect:

  • It to work the same way the other test worked, or
  • For the happy path to also fail.

But when the happy path (e.g. valid mock) passes, but the test fails because the method is invalid (the same one that passes the same validation as part of the "valid" mock)...it leaves me completely confused.

For Reference

Answer

klaasjan69 picture klaasjan69 · Nov 15, 2020

I don´t think you need to test TryGetValue In the library.

Put the BeAnInt in a separate class say XLCellHelpers, test this using a mock of IXLCell

Create an interface for XLCellHelpers, say IXLCellHelpers, inject that into your validator: InvoiceDetailsWorksheetRowValidator

Mock IXLCellHelpers to test the validator.

Like this:

using System;
                    
public class InvoiceDetailsWorksheetRowValidator : AbstractValidator<IXLRow>
{
private readonly IXlCellHelpers xlCellHelpers;

    InvoiceDetailsWorksheetRowValidator(IXlCellHelpers xlCellHelpers)
    {
        this.xlCellHelpers = xlCellHelpers;
    }
    
    public InvoiceDetailsWorksheetRowValidator()
    {
        RuleFor(x => x.Cell("B"))
            .Must(this.xlCellHelpers.BeAnInt).WithMessage("ClaimantID column value is not a number.")
            .OverridePropertyName("ClaimantIDColumn");
    }
}

public interface IXlCellHelpers
{
    bool BeAnInt(IXLCell cellToCheck);
}

public class XlCellHelpers : IXlCellHelpers
{   
    publi bool BeAnInt(IXLCell cellToCheck)
    {
        int result;
        var successful = cellToCheck.TryGetValue(out result);
        return successful;
    }
}