【问题标题】:Moq Unit test fails when calling a Service调用服务时最小起订量单元测试失败
【发布时间】:2014-06-10 17:18:28
【问题描述】:

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

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

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

这是一个使用 xUnit 1.9.2、Moq 4.2 的 MVC5 EF6 应用程序。 EF6 模拟数据库设置如本文Testing with a mocking framework (EF6 onwards)。对于这篇文章,我已经简化了代码,我可以并且没有包含正在工作且不需要显示的内容。

我对为什么在调用服务时测试失败感到困惑,并且由于无法单步执行代码,我不知道如何进一步排除故障。

服务接口:

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。问题是服务没有通过传递模拟数据库的构造函数进行初始化。改变

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

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

现在它通过了测试,生产应用仍然可以运行。

【问题讨论】:

  • 语法高亮有问题吗?
  • 您所说的“服务”是什么意思,究竟?它只是一种类吗?还是像 Web 服务这样的外部系统?
  • 当这个测试失败时你会得到什么异常?

标签: c# unit-testing moq


【解决方案1】:

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

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

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

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

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

现在您可以为服务和数据库传递模拟,并正确设置您的测试场景。作为旁注,您注意到您只是将一个数据库传递给控制器​​,只是为了将其传递给服务。看起来db应该是服务类的依赖和控制器的依赖。

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

上构建你的控制器来做到这一点

【讨论】:

  • 啊!太明显了,谢谢你的第二组眼球。您是正确的,我在传递模拟数据库时未能初始化服务。不过,解决方案要简单得多。只需将 ': this(new BusAcntService())' 添加到传递模拟数据库的控制器的构造函数中。 '公共 BusAcntController(MyDb db) : this(new BusAcntService()) { _db = db; }'。它现在通过了测试,并且仍然可以与真实的数据库一起使用。出于我的目的,我认为没有必要模拟该服务。你也是对的,这更像是一个集成测试。我会将此标记为答案并在 OP 中添加解决方案说明。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-09
  • 1970-01-01
  • 2016-10-04
相关资源
最近更新 更多