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

本文关键字:单元测试 失败 Moq 服务 调用 | 更新日期: 2023-09-27 17:59:55

我正在对我的BusAcnts控制器进行单元测试,以改进生产代码。该视图包含一个WebGrid,我使用Stuart Leeks WebGrid服务代码(_busAcntService.GetBusAcnts)来处理分页和排序。

单元测试失败,错误为"System.NullReferenceExceptionObject引用未设置为对象的实例。"。如果我在调试中运行测试,并在控制器中调用服务的点上设置断点,并在调用方法(GetBusAcnts)上的服务中设置另一个断点,并尝试在调用服务的点将测试失败(具有相同的NullReference错误)。我无法进入服务查看问题的根源。

出于测试目的,我从服务中提取了基本查询,并将其放入控制器中的GetBusAcnts方法中,以模拟服务的大部分功能。当我调用控制器中的GetBusAcnts方法而不是服务中的方法时,测试通过了。

这是一个MVC5 EF6应用程序,使用xUnit 1.9.2,Moq 4.2。EF6 mock数据库的设置与本文中使用mocking框架进行测试(EF6以后)一样。对于这篇文章,我已经简化了代码,其中我可以也没有包括正在工作且不需要显示的内容。

我不知道为什么在调用服务时测试失败,也不知道如何进一步排除故障,因为我无法逐步完成代码。

服务接口:

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

服务:

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();  }
    }
  }

控制器:

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();
  }
}

单元测试:

[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);
}

有人知道为什么对服务的调用导致测试失败吗?或者有人知道我如何进一步排除故障的建议吗?

解决方案:

多亏了Daniel J.G.,问题是没有通过传递mock db的构造函数初始化服务。更改

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

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

它现在通过了测试,生产应用程序仍然可以工作。

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

它抛出该异常,因为您正在使用一个只设置_db的构造函数构造控制器,而_busAcntService保留其默认值(null)。因此,由于_busAcntService为空,测试将在此时失败var busAcnts = _busAcntService.GetBusAcnts(...);

//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; }

在测试中,应该为被测试类的所有依赖项提供mock/stub,并且该类应该提供一些设置这些依赖项的方法(比如构造函数方法中的参数)。

您可以将构造函数更新为:

public BusAcntController() : this(new BusAcntService(), new MyDb())
{ 
} 
public BusAcntController(IBusAcntService busAcntService, MyDb db)
{ 
    _busAcntService = busAcntService;
    _db = db;  
}

然后更新您的测试,向控制器提供服务和数据库实例(这样两者都在您的控制之下,您可以设置您的测试场景):

[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);
}

现在,您可以为服务和数据库传递mock,并正确设置测试场景。顺便说一句,正如您所注意到的,您只是将一个数据库传递给控制器,只是为了将其传递给服务。看起来数据库应该是服务类的依赖项和控制器的依赖项。

最后,从您的原始代码中可以看出,您希望您的代码使用真实的服务实例(而不是模拟服务)运行。如果你真的想这样做(这更像是一个集成测试),你仍然可以通过在你的测试方法var controller = new BusAcntController(new BusAcntService(), mockMyDb.Object); 上构建这样的控制器来做到这一点