调用服务时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; }
它现在通过了测试,生产应用程序仍然可以工作。
它抛出该异常,因为您正在使用一个只设置_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);
上构建这样的控制器来做到这一点