简体   繁体   English

调用服务时Moq单元测试失败

[英]Moq Unit test fails when calling a Service

I'm retrofitting production code with unit testing on my BusAcnts controller. 我正在使用BusAcnts控制器上的单元测试来改造生产代码。 The view contains a WebGrid and I'm using Stuart Leeks WebGrid service code (_busAcntService.GetBusAcnts) to handle the paging and sorting. 该视图包含一个WebGrid,我正在使用Stuart Leeks WebGrid服务代码(_busAcntService.GetBusAcnts)来处理分页和排序。

The Unit test fails with a “System.NullReferenceExceptionObject reference not set to an instance of an object.” error. 单元测试失败,并且“System.NullReferenceExceptionObject引用未设置为对象的实例。”错误。 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. 如果我在调试中运行测试并在控制器中调用服务并在调用方法的服务中调用另一个服务(GetBusAcnts)并尝试逐步完成测试失败(具有相同的NullReference错误)调用服务的重点。 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. 出于测试目的,我从服务中提取了基本查询,并将其放入控制器中的GetBusAcnts方法中,以模拟服务的大部分功能。 When I call the GetBusAcnts method in the controller rather than the one in the service the test passes. 当我在控制器中调用GetBusAcnts方法而不是服务中的方法时,测试通过。

This is a MVC5 EF6 application using xUnit 1.9.2, Moq 4.2. 这是使用xUnit 1.9.2,Moq 4.2的MVC5 EF6应用程序。 The EF6 mock database is set up as in this article Testing with a mocking framework (EF6 onwards) . EF6模拟数据库的设置与本文一样,使用模拟框架进行测试(EF6以上版本) 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 JG The problem was that the service was not being initialized with the constructor passing the mock db. 感谢Daniel JG问题是服务没有通过传递模拟数据库的构造函数进行初始化。 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). 它抛出异常,因为您使用仅设置_db的构造函数构建控制器,使_busAcntService其默认值(null)。 So the test will fail at this point var busAcnts = _busAcntService.GetBusAcnts(...); 所以测试将在此时失败var busAcnts = _busAcntService.GetBusAcnts(...); because _busAcntService is null. 因为_busAcntService为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. 看起来db应该是服务类的依赖项和控制器的依赖项。

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); 如果你真的想这样做(这可能更像是集成测试),你仍然可以通过在测试方法上构建你的控制器来实现这一点var controller = new BusAcntController(new BusAcntService(), mockMyDb.Object);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM