xunit and mock returns null instead of string

50 Views Asked by At

I'm having an xUnit.net and Moq test problem, I have mocked my dependencies for my controller and setup the mock to return a Task<string> but my response.Value is null here. Can someone help with this problem?

namespace TestProject
{
    public class CabinetControllerTest : ControllerBase
    {
        private Mock<IWearerPort>    _canOpenPort;
        private Mock<ITwoTraceLogger>  _twoTraceLogger;
        private Mock<ITransactionPort> _transaction;

        public CabinetControllerTest()
        {
            _canOpenPort = new Mock<IWearerPort>();
            _transaction = new Mock<ITransactionPort>();
            _twoTraceLogger = new Mock<ITwoTraceLogger>();
        }

        [Fact]
        public async void AccessControl_Success_Test()
        {
        //Arrange
         Task<string> task = Task.FromResult<string>("Forbidden");
         _canOpenPort.Setup(c => 
         c.ValidateWearerAsync("BorrowCabinet", 
         "643674")).Returns(task);
          var controller = new 
          CabinetController(_transaction.Object, 
          _canOpenPort.Object, _twoTraceLogger.Object);
          var expected = Ok("Forbidden");

         //Act

         var response = await 
         controller.AccessControl("BorrowCabinet", "643674");

         var actual = Ok(response.Value);

         //Assert
        Assert.Equal(expected, actual);
        _canOpenPort.Verify(c => 
        c.ValidateWearerAsync("BorrowCabinet", "643674"), 
        Times.Once);
        }

Controiller:

[ApiController]
[Route("[controller]")]
public class CabinetController : ControllerBase
{
    private readonly IWearerPort _canOpenPort;
    private readonly ITwoTraceLogger _twoTraceLogger;
    private readonly ITransactionPort _transaction;

    public CabinetController(ITransactionPort transaction, 
    IWearerPort canOpen, ITwoTraceLogger twoTraceLogger)
    {
        _twoTraceLogger = twoTraceLogger;
        _canOpenPort = canOpen;
        _transaction = transaction;
    }

    [HttpGet]
    [Route("[controller]/accesscontrol")]
    public async Task<ActionResult<string>> AccessControl(string 
     clientID, string cardId)
    {
        _twoTraceLogger.LogTrace("CabinetController gets called");
        var result = await 
    _canOpenPort.ValidateWearerAsync(clientID,cardId);
        return Ok(result);
    }
  }
2

There are 2 best solutions below

2
Mark Seemann On

The test is configuring the Test Double to expect other values than the System Under Test (SUT) receives:

_canOpenPort.Setup(c => c.ValidateWearerAsync("borrow", "643674")).Returns(task);

[...]

var response = await controller.AccessControl("BorrowCabinet", "368629398762");

Make sure that those two pairs are the same. If the values don't match the Setup, Moq will return default values, which often means null references.

You may also want to use ReturnsAsync instead of Returns to simplify the test code.


After (ahem) actually having tried to repro the problem, here's a few things you can try.

Delete this line of code:

//var actual = Ok(response.Value);

The object created by Ok doesn't have structural equality anyway, so one can't use it to compare expected with actual - unfortunately, because it's a good idea to use such comparisons for unit tests.

Instead, write assertions like this:

var actual = Assert.IsAssignableFrom<OkObjectResult>(response.Result);
Assert.Equal("Forbidden", actual.Value);

I can't say that I entirely understand why ActionResult<string> works the way it does, but it looks as though its Value is null, whereas the associated Result object's Value property is, indeed, populated by the appropriate value.

In any case, testing like this risks turning into Framework Whac-A-Mole, so if possible, I'd recommend instead testing the API via HTTP.

1
Guru Stron On

Moq will match the values used by the setup. Since CabinetController.AccessControl just passes directly clientID and cardId to ValidateWearerAsync you need to match the values in the setup to ones passed to the controller. For example:

_canOpenPort
    .Setup(c => c.ValidateWearerAsync("BorrowCabinet", "368629398762"))
    .Returns(task);
// ...

// values match the one used for setup
var response = await controller.AccessControl("BorrowCabinet", "368629398762");

Sometimes It.IsAny can be used:

_canOpenPort.Setup(c => c.ValidateWearerAsync(It.IsAny<string>(), It.IsAny<string>()))
    .Returns(task);

but in general I would recommend to avoid it.

Also you need to update the assertion:

var okObjectResult = Assert.IsAssignableFrom<OkObjectResult>(response.Result);
Assert.Equal("Forbidden", okObjectResult.Value);

P.S.

  1. Consider switching to Integration tests in ASP.NET Core for your controllers, it can reduce the need to deep dive into ASP.NET Core infrastructure (not always though) and will allow to validate actual service response (do not be afraid of "integration" word here, you can still consider them unit ones depending on the infrastructure used and what is considered to be the unit).
  2. Returns(task) can be changed to ReturnsAsync with value:
 _canOpenPort.Setup(c => c.ValidateWearerAsync(...))
    .ReturnsAsync("Forbidden");