我正在使用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; }
它现在通过测试,生产应用程序仍然有效。
答案 0 :(得分:2)
抛出该异常是因为您使用仅设置_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);