I’m retrofitting production code with unit testing on my BusAcnts controller. The view contains a WebGrid and I’m using Stuart Leeks WebGrid service code (_busAcntService.GetBusAcnts) to handle the paging and sorting.

The Unit test fails with a “System.NullReferenceExceptionObject reference not set to an instance of an object.” error. If I run the test in debug and put a breakpoint at the point the service is called in the controller and another one in the Service on the called method (GetBusAcnts) and try to step through the test fails (with the same NullReference error) at the point the service is called. I cannot step into the service to see what the source of the problem is.

For testing purposes I pulled the basic query out of the service and put it in a GetBusAcnts method in the controller to emulate most of the function of the service. When I call the GetBusAcnts method in the controller rather than the one in the service the test passes.

This is a MVC5 EF6 application using xUnit 1.9.2, Moq 4.2. The EF6 mock database is set up as in this article Testing with a mocking framework (EF6 onwards). For this post I've simplified the code where I could and have not included things that are working and don't need to be shown.

I’m stumped as to why the test is failing at the point the service is called and don’t know how to troubleshoot further since I can’t step through the code.

Service Interface:

public interface IBusAcntService
{
   IEnumerable<BusIdxVm> GetBusAcnts(MyDb dbCtx, out int totalRecords,
   int pageSize = -1, int pageIndex = -1, string sort = "Name",
   SortDirection sortOrder = SortDirection.Ascending); 
}

Service:

public class BusAcntService : IBusAcntService
{
    // helpers that take an IQueryable<TAFIdxVM> and a bool to indicate ascending/descending
    // and apply that ordering to the IQueryable and return the result
    private readonly IDictionary<string, Func<IQueryable<BusIdxVm>, bool,
                   IOrderedQueryable<BusIdxVm>>>
      _busAcntOrderings = new Dictionary<string, Func<IQueryable<BusIdxVm>, bool,
                   IOrderedQueryable<BusIdxVm>>>
        {
          {"AcntNumber", CreateOrderingFunc<BusIdxVm, int>(p=>p.AcntNumber)},
          {"CmpnyName", CreateOrderingFunc<BusIdxVm, string>(p=>p.CmpnyName)},
          {"Status", CreateOrderingFunc<BusIdxVm, string>(p=>p.Status)},
          {"Renewal", CreateOrderingFunc<BusIdxVm, int>(p=>p.Renewal)},
          {"Structure", CreateOrderingFunc<BusIdxVm, string>(p=>p.Structure)},
          {"Lock", CreateOrderingFunc<BusIdxVm, double>(p=>p.Lock)},
          {"Created", CreateOrderingFunc<BusIdxVm, DateTime>(t => t.Created)},
          {"Modified", CreateOrderingFunc<BusIdxVm, DateTime>(t => t.Modified)}
        };
      /// <summary>
      /// returns a Func that takes an IQueryable and a bool, and sorts the IQueryable
      ///                 (ascending or descending based on the bool).
      /// The sort is performed on the property identified by the key selector.
      /// </summary>
      /// <typeparam name="T"></typeparam>
      /// <typeparam name="TKey"></typeparam>
      /// <param name="keySelector"></param>
      /// <returns></returns>

    private static Func<IQueryable<T>, bool, IOrderedQueryable<T>> CreateOrderingFunc<T,
                         TKey>(Expression<Func<T, TKey>> keySelector)
    { 
       return  (source, ascending) =>  ascending ? source.OrderBy(keySelector) :
                   source.OrderByDescending(keySelector);
    }

    public IEnumerable<BusIdxVm> GetBusAcnts(MyDb dbCtx, out int totalRecords,
          int pageSize = -1, int pageIndex = -1, string sort = "Name",
          SortDirection sortOrder = SortDirection.Ascending)
    {
      using (var db = dbCtx) { IQueryable<BusIdxVm> ba;
      ba = from bsa in db.BusAcnts select new BusIdxVm { Id = bsa.Id,
           AcntNumber = bsa.AcntNumber, CmpnyName = bsa.CmpnyName, Status = bsa.Status,
           Renewal = bsa.RnwlStat, Structure = bsa.Structure, Lock = bsa.Lock,
           Created = bsa.Created,Modified = bsa.Modified };
      totalRecords = ba.Count();
      var applyOrdering = _busAcntOrderings[sort]; // apply sorting
      ba = applyOrdering(ba, sortOrder == SortDirection.Ascending);
      if (pageSize > 0 && pageIndex >= 0)  // apply paging
      {
        ba = ba.Skip(pageIndex * pageSize).Take(pageSize);
      }
      return ba.ToList();  }
    }
  }

Controller:

public class BusAcntController : Controller
{
  private readonly MyDb _db;
  private readonly IBusAcntService _busAcntService;

  public BusAcntController() : this(new BusAcntService())
  { _db = new MyDb(); } 

  public BusAcntController(IBusAcntService busAcntService)
  { _busAcntService = busAcntService; }

  public BusAcntController(MyDb db) { _db = db; }

  public ActionResult Index(int page = 1, string sort = "AcntNumber", 
                            string sortDir = "Ascending")
  { 
    int pageSize = 15;
    int totalRecords;
    var busAcnts = _busAcntService.GetBusAcnts( _db, out totalRecords,
                   pageSize: pageSize, pageIndex: page - 1, sort: sort,
                   sortOrder: Mth.GetSortDirection(sortDir));
    //var busAcnts = GetBusAcnts(_db);   //Controller method
    var busIdxVms = busAcnts as IList<BusIdxVm> ?? busAcnts.ToList();
    var model = new PagedBusIdxModel { PageSize = pageSize, PageNumber = page,
                    BusAcnts = busIdxVms, TotalRows = totalRecords };
    ViewBag._Status = Mth.DrpDwn(DropDowns.Status, ""); ViewBag._Lock = Mth.DrpDwn
    return View(model);
  }

  private IEnumerable<BusIdxVm> GetBusAcnts(MyDb db)
  {
    IQueryable<BusIdxVm> ba = from bsa in db.BusAcnts select new BusIdxVm
    {
      Id = bsa.Id,  AcntNumber = bsa.AcntNumber,  CmpnyName = bsa.CmpnyName,
      Status = bsa.Status, Renewal = bsa.RnwlStat, Structure = bsa.Structure,
      Lock = bsa.Lock,  Created = bsa.Created, Modified = bsa.Modified
    };
    return ba.ToList();
  }
}

Unit Test:

[Fact]
public void GetAllBusAcnt()
{
  var mockMyDb = MockDBSetup.MockMyDb();
  var controller = new BusAcntController(mockMyDb.Object);
  var controllerContextMock = new Mock<ControllerContext>();
  controllerContextMock.Setup(
      x => x.HttpContext.User.IsInRole(It.Is<string>(s => s.Equals("admin")))
      ).Returns(true);
  controller.ControllerContext = controllerContextMock.Object;

  var viewResult = controller.Index() as ViewResult;
  var model = viewResult.Model as PagedBusIdxModel;

  Assert.NotNull(model);
  Assert.Equal(6, model.BusAcnts.ToList().Count());
  Assert.Equal("Company 2", model.BusAcnts.ToList()[1].CmpnyName);
}

Does anyone have any idea why the call to the service is making the test fail or suggestions on how I might troubleshoot further?

Solution:

Thanks to Daniel J. G. The problem was that the service was not being initialized with the constructor passing the mock db. Change

public BusAcntController(MyDb db) { _db = db; }

to

public BusAcntController(MyDb db) : this(new BusAcntService()) { _db = db; }

It now passes the test and the production app still works.

有帮助吗?

解决方案

It is throwing that exception because you are constructing your controller using a constructor that only sets _db, leaving _busAcntService with its default value (null). So the test will fail at this point var busAcnts = _busAcntService.GetBusAcnts(...); because _busAcntService is null.

//In your test you create the controller using:
var controller = new BusAcntController(mockMyDb.Object);

//which calls this constructor, that only sets _db:
public BusAcntController(MyDb db) { _db = db; }

In your tests you should provide mocks/stubs for all dependencies of the class under test, and that class should provide some means to set those dependencies (like parameters in constructor methods).

You could update your constructors as:

public BusAcntController() : this(new BusAcntService(), new MyDb())
{ 
} 

public BusAcntController(IBusAcntService busAcntService, MyDb db)
{ 
    _busAcntService = busAcntService;
    _db = db;  
}

Then update your test to provide both the service and db instances to the controller (so both are under your control and you can setup your test scenario):

[Fact]
public void GetAllBusAcnt()
{
    var mockMyDb = MockDBSetup.MockMyDb();

    //create a mock for the service, and setup the call for GetBusAcnts
    var serviceMock = new Mock<IBusAcntService>();
    var expectedBusAccounts = new List<BusIdxVm>(){ new BusIdxVm(), ...a few more...  };
    serviceMock.Setup(s => s.GetBusAcnts(mockMyDb.Object, ....other params...)).Returns(expectedBusAccounts);

    //Create the controller using both mocks
    var controller = new BusAcntController(serviceMock.Object, mockMyDb.Object);
    var controllerContextMock = new Mock<ControllerContext>();
    controllerContextMock.Setup(
      x => x.HttpContext.User.IsInRole(It.Is<string>(s => s.Equals("admin")))
      ).Returns(true);
    controller.ControllerContext = controllerContextMock.Object;

    var viewResult = controller.Index() as ViewResult;
    var model = viewResult.Model as PagedBusIdxModel;

    Assert.NotNull(model);
    Assert.Equal(6, model.BusAcnts.ToList().Count());
    Assert.Equal("Company 2", model.BusAcnts.ToList()[1].CmpnyName);
}

Now you can pass mocks for both the service and database, and setup correctly your test scenario. As a sidenote, as you notice you are just passing a db to the controller, just to pass it to the service. It looks like the db should be a dependency of the service class and a dependency of the controller.

Finally, it looks from your original code that you were expecting your code to run with a real service instance (and not a mocked service). If you really want to do so (which would be more of an integration test), you can still do that by building your controller like this on your test method var controller = new BusAcntController(new BusAcntService(), mockMyDb.Object);

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top