Search code examples
unit-testingxunitasp.net-web-api-odata

Unit Test OData V4 PUT action with XUnit and MOQ


My goal is to unit test the PUT action in an OData v4 controller.

I'm using MOQ for the Entity Framework 6 Context and NBuilder to build out the test data.

I am able to successfully test Get and Get(Id), but am unable to run asserts when I retrieve the HTTPActionResult from the PUT action.

I can see the HTTPActionResult returning an UpdatedODataResult object with an Entity property in debug mode, but I don't see a way to work with it at design time.

Does anyone know how to extract the returned Entity from the Async PUT action response?

Here is the code:

using Models;
using WebApp.DAL;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;

namespace WebApp.Controllers.Api
{
    public class OrgsController : ODataController
    {
        private IWebAppDbContext db = new WebAppDbContext();

        public OrgsController()
        {
        }

        public OrgsController(IWebAppDbContext Context)
        {
           db = Context;
        }

        public async Task<IHttpActionResult> Put([FromODataUri] long Key, Org Entity)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            if (Key != Entity.Id)
            {
                return BadRequest();
            }

            db.MarkAsModified(Entity);
            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!EntityExists(Key))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
            return Updated(Entity);
        }
        //...other actions omitted
    }
}

Here is my Unit Test code

[Theory, InlineData(5)]
public async Task Api_Put_Updates_Properties(long Id)
{            
    //arrange
    var mockedDbContext = MocksFactory.GetMockContext<WebAppDbContext>();
    mockedDbContext.Object.Orgs.AddRange(MocksFactory.GetMockData<Org>(10));                
    OrgsController _sut = new OrgsController(mockedDbContext.Object);
    Org beforeEntity = new Org
    {
        Id = Id,
        Name = "Put Org",
        TaxCountryCode = "PutUs",
        TaxNumber = "PutUs01"
    };

    //act
    IHttpActionResult actionResult = await _sut.Put(Id, beforeEntity); 

    //assert   
    Assert.NotNull(actionResult);
    //Assert.NotNull(actionResult.Entity);
    //Assert.Equal(Id, actionResult.Entity.Id);
    //Assert.Equal("Put Org", actionResult.Entity.Name);
}

Solution

  • Thank you @Old Fox for the suggestion. Here's what I ended up with: *Note: I included in-line mock setup for explanation purposes. I used this approach to create my factories in the production code: http://www.rhyous.com/2015/04/10/how-to-mock-an-entity-framework-dbcontext-and-its-dbset-properties/#comment-121594

    This post also helped me piece together the Moq setup: How to moq Entity Framework SaveChangesAsync?

    Also, the link I referenced in the comments shows how to properly setup the GetEnumerator method. The instructions on the MSDN page are incorrect. The difference is subtle, but significant (that discovery cost me a week). Make sure you set that piece up properly or your context will have an empty dbset (Orgs in this example).

    [Theory, InlineData(1), InlineData(3), InlineData(5)]
    public async Task Api_Put_Valid_Entity_Calls_ContextMethods_And_Returns_UpdatedODataResult(long Key)
    {
        //arrange
        var data = new List<Org>
        {
            new Org { Id = 1, Name = "Name1", TaxCountryCode = "T1", TaxNumber = "TaxNumber1", CreatedBy = 1, CreatedOn = DateTime.Now, ModifiedBy = 1, ModifiedOn = DateTime.Now, Accounts = null, ChargeRequestsFromMe = null, ChargeRequestsToMe = null, PaymentRequestsFromMe = null, PaymentRequestsToMe = null, Submissions = null },
            new Org { Id = 2, Name = "Name2", TaxCountryCode = "T2", TaxNumber = "TaxNumber2", CreatedBy = 1, CreatedOn = DateTime.Now, ModifiedBy = 1, ModifiedOn = DateTime.Now, Accounts = null, ChargeRequestsFromMe = null, ChargeRequestsToMe = null, PaymentRequestsFromMe = null, PaymentRequestsToMe = null, Submissions = null },
            new Org { Id = 3, Name = "Name3", TaxCountryCode = "T3", TaxNumber = "TaxNumber3", CreatedBy = 1, CreatedOn = DateTime.Now, ModifiedBy = 1, ModifiedOn = DateTime.Now, Accounts = null, ChargeRequestsFromMe = null, ChargeRequestsToMe = null, PaymentRequestsFromMe = null, PaymentRequestsToMe = null, Submissions = null },
            new Org { Id = 4, Name = "Name4", TaxCountryCode = "T4", TaxNumber = "TaxNumber4", CreatedBy = 1, CreatedOn = DateTime.Now, ModifiedBy = 1, ModifiedOn = DateTime.Now, Accounts = null, ChargeRequestsFromMe = null, ChargeRequestsToMe = null, PaymentRequestsFromMe = null, PaymentRequestsToMe = null, Submissions = null },
            new Org { Id = 5, Name = "Name5", TaxCountryCode = "T5", TaxNumber = "TaxNumber5", CreatedBy = 1, CreatedOn = DateTime.Now, ModifiedBy = 1, ModifiedOn = DateTime.Now, Accounts = null, ChargeRequestsFromMe = null, ChargeRequestsToMe = null, PaymentRequestsFromMe = null, PaymentRequestsToMe = null, Submissions = null }
        }.AsQueryable();
    
        var mockSet = new Mock<DbSet<Org>>();
        mockSet.As<IQueryable<Org>>().Setup(m => m.Provider).Returns(data.Provider);
        mockSet.As<IQueryable<Org>>().Setup(m => m.Expression).Returns(data.Expression);
        mockSet.As<IQueryable<Org>>().Setup(m => m.ElementType).Returns(data.ElementType);
        mockSet.As<IQueryable<Org>>().Setup(m => m.GetEnumerator()).Returns(() => data.GetEnumerator());
        mockSet.Setup(m => m.Find(It.IsAny<object[]>())).Returns<object[]>(ids => data.FirstOrDefault(d => d.Id == (int)ids[0]));            
    
        var mockContext = new Mock<MyDbContext>();
        mockContext.Setup(m => m.Orgs).Returns(mockSet.Object);
        mockContext.Setup(m => m.SaveChangesAsync()).Returns(() => Task.Run(() => { return 1; })).Verifiable();
        mockContext.Setup(m => m.MarkAsModified(It.IsAny<Org>())).Verifiable();
    
        Org entity = new Org
        {
            Id = Key,
            Name = "Put Org",
            TaxCountryCode = "Put" + Key.ToString(),
            TaxNumber = "Put" + Key.ToString()
        };
    
        var sut = new OrgsController(mockContext.Object);
    
        //act
        var actionResult = await sut.Put(Key, entity) as UpdatedODataResult<Org>;
    
        //assert
        mockContext.Verify(m => m.SaveChangesAsync(), Times.Once());
        mockContext.Verify(m => m.MarkAsModified(entity), Times.Once());
        Assert.IsType<UpdatedODataResult<Org>>(actionResult);
        Assert.Equal(entity.Name, actionResult.Entity.Name);
        Assert.Equal(entity.TaxCountryCode, actionResult.Entity.TaxCountryCode);
        Assert.Equal(entity.TaxNumber, actionResult.Entity.TaxNumber);
    }