2

I am new to Unit Testing and am trying to create some Xunit tests for my Web API Controller's POST method.

Here is my Controller's POST method:

[HttpPost("")]
public async Task<IActionResult> CreateArea([FromBody] AreaForCreationDto area)
{
    // Check that the 'area' object parameter can be de-serialised to a AreaForCreationDto.
    if (area == null)
    {
        var message = "Could not de-serialise the request body to an AreaForCreationDto object";
        _logger.LogError(message);

        // Return an error 400.
        return BadRequest(message);
    }

    /*
     * ModelState.IsValid is determined by the attributes associated with the 
     * Data Annotations on the properties of the ViewModel.
     */
    if (!ModelState.IsValid)
    {
        // Return a response with a Status Code 422.
        return new UnprocessableEntityObjectResult(ModelState);
    };

    // Map a AreaForCreationDto object to a Area entity.
    var areaEntityToAdd = _mapper.Map<Area>(area);

    // Call the repository to add the new Area entity to the DbContext.
    _areaRepository.AddArea(areaEntityToAdd);

    // Save the new Area entity, added to the DbContext, to the SQL database.
    if (await _areaRepository.SaveChangesAsync())
    {
        // Note: AutoMapper maps the values of the properties from the areaEntityToAdd
        // to a new areaToReturn object.
        // This ensures that we don't expose our Area entity to the web browser.
        var areaToReturn = _mapper.Map<AreaDto>(areaEntityToAdd);

        // Return a 201 'created' response along with the location URL in the
        // response Header.
        return CreatedAtRoute("GetArea",
            new { id = areaToReturn.Id },
            areaToReturn);
    }
    else {
        // The save failed.
        var message = $"Could not save new Area {areaEntityToAdd.Id} to the database.";
        _logger.LogWarning(message);
        throw new Exception(message);
    };
}

The first Unit Test I have written is intended to ensure that on sending a POST request, with an object which can be de-serialised into an AreaForCreation object, the function returns a 201 CreatedAtRouteResult along with the new Area which has been created.

Here is the Xunit test:

[Fact]
public void ReturnAreaForCreateArea()
{
    //Arrange

    var _mockAreaRepository = new Mock<IAreaRepository>();
    _mockAreaRepository
        .Setup(x => x.AddArea(testArea));

    var _mockMapper = new Mock<IMapper>();
    _mockMapper
        .Setup(_ => _.Map<Area>(It.IsAny<AreaForCreationDto>()))
        .Returns(testArea);

    var _mockLogger = new Mock<ILogger<AreasController>>();
    var _sut = new AreasController(_mockAreaRepository.Object, _mockLogger.Object, _mockMapper.Object);

    // Act
    var result = _sut.CreateArea(testAreaForCreationDto);

    // Assert
    Assert.NotNull(result);
    var objectResult = Assert.IsType<CreatedAtRouteResult>(result);
    var model = Assert.IsAssignableFrom<AreaDto>(objectResult.Value);
    var areaDescription = model.Description;
    Assert.Equal("Test Area For Creation", areaDescription);
}

I am getting an exception thrown when the unit test tries to Assert.IsType<CreatedAtRouteResult>(result). Debugging revealed that the Controller was failing to save to the repository. My AreaRepository has the following AddArea function which does not return a value so I assume that my _mockAreaRepository does not require a Return condition set (could be wrong here).

Do I need to configure my mockAreasRepository for the outcome of calling SaveChangesAsync()?

1
  • Yes because it is async you need to mock the return of a completed task to allow the method to be able to continue. You also need to update the test to be async as well by return a Task and await the method under test Commented Jan 24, 2018 at 16:05

1 Answer 1

2

Yes because it is async you need to mock the return of a completed task to allow the method

await _areaRepository.SaveChangesAsync()

to be able to continue.

You also need to update the test to be async as well by returning a Task and await the method under test.

[Fact]
public async Task ReturnAreaForCreateArea() { //<-- note test is now async as well
    //Arrange

    var _mockAreaRepository = new Mock<IAreaRepository>();
    _mockAreaRepository
        .Setup(x => x.AddArea(testArea));

    _mockAreaRepository
        .Setup(x => x.SaveChangesAsync())
        .ReturnsAsync(true); //<-- returns completed Task<bool> when invoked

    var _mockMapper = new Mock<IMapper>();
    _mockMapper
        .Setup(_ => _.Map<Area>(It.IsAny<AreaForCreationDto>()))
        .Returns(testArea);
    _mockMapper
        .Setup(_ => _.Map<AreaDto>(It.IsAny<Area>()))
        .Returns(testAreaDto);

    var _mockLogger = new Mock<ILogger<AreasController>>();
    var _sut = new AreasController(_mockAreaRepository.Object, _mockLogger.Object, _mockMapper.Object);

    // Act
    var result = await _sut.CreateArea(testAreaForCreationDto);//<-- await 

    // Assert
    Assert.NotNull(result);
    var objectResult = Assert.IsType<CreatedAtRouteResult>(result);
    var model = Assert.IsAssignableFrom<AreaDto>(objectResult.Value);
    var areaDescription = model.Description;
    Assert.Equal("Test Area For Creation", areaDescription);
}
Sign up to request clarification or add additional context in comments.

7 Comments

Long delay as I was diverted onto another project. We finally tried your very helpful advise but the very first Assertion (.NotNull) failed. The Controller saves the newly created Area entity to the Repository and then returns an Area which is created by calling the function at the GetArea route, passing in the new Id. Do I need to configure the GetArea function of the Controller in order for this to work?
Then something has changed in your code from what you showed originally. The logic in your code does not allow for the returning of null.
True, our controller is only returning a CreatedAtRoute if the SaveChangesAsync() is successful, if it fails we have not provided for the return of a Error Code 500 so that we should never have null returned. I will continue to try and see what is going wrong during the save.
If my await _areaRepository.SaveChangesAsync() returns true, then my controller under test maps an AreaEntityToAdd to an AreaDTO and passes this in to the GetArea method of the controller along with the new Areas id. I haven't accounted for the mapping and repository actions within the GetArea function in my _mockMapper and _mockRepository. Do I need to?
Yes for the mapping
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.