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.
I know there's a wall of text below, so the question (in advance) is:
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));
}
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 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:
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.
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;
}
}