0

I am fairly new to programming and only know the basics.

I am currently writing a unit test for a .net core 2.2 controller. I am using Rider, Nunit, and Moq.

Here is one of the unit test where I am getting a problem:

    [Test]
    [TestCase(true)]
    [TestCase(false)]
    public async Task GetPersonAsync_PersonDoesExist_ReturnOk(bool includeRelated)
    {
        _repository.Setup(r => r.GetPersonAsync(_id, includeRelated)).ReturnsAsync(_person);

        _mapper.Setup(m => m.Map<Person, GetPersonResource>(It.IsAny<Person>())).Returns(_getPersonResource);

        var result = await _controller.GetPersonAsync(_id);

        Assert.That(result, Is.TypeOf<OkObjectResult>());
    }

Here is my constructor/top part and setup of that unit test:

public class PersonsControllerTests
{
    private Mock<IMapper> _mapper;
    private Mock<IUnitOfWork> _unitOfWork;
    private Mock<IClientsRepository> _repository;
    private PersonsController _controller;
    private int _id;
    private Person _person;
    private GetPersonResource _getPersonResource;
    private CreatePersonResource _createPersonResource;
    private UpdatePersonResource _updatePersonResource;

    [SetUp]
    public void Setup()
    {
        _mapper = new Mock<IMapper>();
        _unitOfWork = new Mock<IUnitOfWork>();
        _repository = new Mock<IClientsRepository>();

        _controller = new PersonsController(_mapper.Object, _repository.Object, _unitOfWork.Object);

        _id = 1;
        _person = new Person {Id = 1};
        _getPersonResource = new GetPersonResource {Id = 1};
        _createPersonResource = new CreatePersonResource {Code = "a"};
        _updatePersonResource = new UpdatePersonResource {Code = "a"};
    }

Here is my GetPersonAsync method in my controller:

    [HttpGet("{id}")]
    public async Task<IActionResult> GetPersonAsync(int id)
    {
        var person = await repository.GetPersonAsync(id);

        if (person == null)
            return NotFound();

        return Ok(mapper.Map<Person, GetPersonResource>(person));
    }

Here is my constructor/top part for my controller:

[Route("/api/persons")]
public class PersonsController : Controller
{
    public IMapper mapper { get; }
    private readonly IClientsRepository repository;
    private readonly IUnitOfWork unitOfWork;

    public PersonsController(IMapper mapper, IClientsRepository repository, IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
        this.repository = repository;
        this.mapper = mapper;
    }

Here is my GetPersonAsync method in my Repository:

    public async Task<Person> GetPersonAsync(int id, bool includeRelated = true) 
    {
        if (!includeRelated)
            return await context.Persons
                .FirstOrDefaultAsync(p => p.Id == id);
        else
            return await context.Persons
                .Include(p => p.Clients)
                .Include(p => p.ClientRelateds)
                    .ThenInclude(cr => cr.ClientRelationType)
                .FirstOrDefaultAsync(p => p.Id == id);
    }

Here is my GetPersonAsync signature in my IRepository:

Task<Person> GetPersonAsync(int id, bool includeRelated = true);

The problem that I am encountering is that whenever I run a test and change the includeRelated argument to false, which is true by default, my tests fail.

For example, in the test that I have given above my test with test case false, it should return Ok but is returning NotFound, when I use the true test case it returns Ok and passes which is what I want. I also get the same when I want to see if a model is saved. My test where includeRelated is set to true invokes my _unitOfWork.CompleteAsync method but does not invoke it when includeRelated is set to false.

When I set the default value of include related to false in my repository all my test cases with includeRelated true fails and includeRelated false passes.

There is a big chance that I either made a mistake in my project or in my unit test but it seems to me like, and correct me if I'm wrong, there is a bug with Moq as I am Mocking the repository and the actual implementation of the repository (i.e the default value of true for includeRelated) does not matter at all.

Johnny
  • 8,939
  • 2
  • 28
  • 33
  • 1
    The controller is not calling repository's method with `includeRelated = false` ever. – Chetan Oct 16 '19 at 12:22
  • Related to the comment above, usually you'd use the TestCase attribute to combine some input value with some expected output value (for example, see the [docs here](https://nunit.org/docs/2.6/testCase.html) ). I mean, in you're case, you're essentially declaring "no matter what the input is, it should return the same output". – chill94 Oct 16 '19 at 12:28
  • Ok, I don't fully understand however if you don't mind, could you give me an idea of how and what I should refactor to have my tests pass or if I should just remove the tests which test with the includeRelated set to false? – Drikus van Blerk Oct 16 '19 at 13:09

1 Answers1

0

Just to be clear, the code for your repository is irrelevant here, because you are mocking it - that code is never executed. Instead, results are returned for the calls that you set up - all other calls return the .NET default for the return type - generally null in your case.

In your setup, you have conditioned the repository mock to return a value only when called with arguments (1, true) or (1, false), depending on which test case is running. Any time it is called with just the argument (1) by the SUT, true will be used even if your intention is to test the false case. Person will be returned as null. IOW, you have to set up the calls that will actually be used by the controller. For a robust test, you should set up different return values for both possibilities.

Testing methods with default arguments is notoriously tricky. You have to allow for the possibility that the SUT may not always make use of the argument you expect, but may in fact use the default.

Minor nitpick: You don't need [Test] and IMO it's ugly and confusing in this context - makes it look at first glance as if there are three test cases rather than two.

Charlie
  • 12,928
  • 1
  • 27
  • 31
  • Actually, the method *will* be called with an includeRelated parameter, it will just be `true`, which is the default value provided by the signature. Omitting optional parameter when calling a method that has them does not actually omit the optional parameters, it just leaves it to the compiler to fill in the blanks. – Lasse V. Karlsen Oct 16 '19 at 14:16
  • Right... only two possibilities to set up then. I'll edit the answer. – Charlie Oct 17 '19 at 15:47