单元测试控制器 - 如何处理连接字符串?
Unit Testing a Controller - How Do I Handle the Connection String?
我可以让它工作,但我想知道最佳做法是什么以及为什么。我有一个控制器、一个模型和一个存储库,现在我想对控制器进行单元测试。我只是在编写一个简单的测试以确保返回正确的视图。
这是我在控制器中的方法:
public ActionResult Selections(SelectionsViewModel model)
{
for (int i = 0; i < model.Sends.Count; i++)
{
Send send = new Send(new SendService(new Database().GetConnectionString()))
{
SendID = model.Sends[i].SendID,
Title = model.Sends[i].Title,
Subject = model.Sends[i].Subject,
SentDate = model.Sends[i].SentDate,
TimesViewed = model.Sends[i].TimesViewed,
Include = model.Sends[i].Include,
Exclude = model.Sends[i].Exclude
};
send.UpdateIncludeExclude();
}
return View(model);
}
这是数据库 class 中我的 GetConnectionString() 方法,它通过我的 SendService 构造函数发送。
public string GetConnectionString()
{
return System.Configuration.ConfigurationManager.ConnectionStrings["DEVConnectionString"].ToString();
}
最后,我的单元测试:
[Test]
public void TestAssignmentSelections()
{
var obj = new AssignmentController();
var actResult = obj.Selections() as ViewResult;
NUnit.Framework.Assert.That(actResult.ViewName, Is.EqualTo("Selections"));
}
现在,我的单元测试失败了,我知道原因了。我的单元测试项目无权访问我正在测试的项目的 web.config 我的连接字符串所在的位置。
我做了一些研究,显然只要在我的单元测试项目中添加一个 web.config 并将连接字符串也放在那里就可以使它工作..但这似乎是一个 hack。
解决此问题的最佳方法是什么?有没有另一种方法来编写我的代码来适应这个?
您想使您的控制器单元可测试吗?不要这样做。
new SendService(
使用此代码,您将对具体服务实现和数据访问代码实现进行硬编码。在您的单元测试中,您不应该真正访问数据库中的数据。相反,您应该提供一个模拟数据访问实现。
接口来了,你需要为你的SendService
创建一个接口。
public interface ISendService
{
void SomeMethod();
}
现在你的SendService
将是这个接口的具体实现
public class SendService : ISendService
{
public void SomeMethod()
{
// Do something
}
}
现在更新您的控制器以拥有一个构造函数,我们将在其中注入 ISendService
.
的实现
public class YourController : Controller
{
private ISendService sendService;
public YourController(ISendService sendService)
{
this.sendService = sendService;
}
public ActionResult YourActionMethod()
{
// use this.sendService.SomeMethod();
}
}
并且您可能会使用一些依赖注入框架来告诉 MVC 框架在代码运行时使用哪个接口实现。如果你使用的是 MVC6,它有一个你可以使用的内置依赖注入提供程序。因此,转到 Startup
class 并在 ConfigureServices
方法中,您可以将接口映射到具体实现。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISendService, SendService>();
}
}
如果您使用的是以前版本的 MVC,您可以考虑像 Unity 这样的 DI 框架,Ninject 等。您可以对稍后的数据访问/服务层执行相同的方法。即:创建一个用于数据访问的接口并将其注入您的 SendService。
public Interface IDataAccess
{
string GetName(int id);
}
和一个使用您的特定数据访问的实现 code/ORM
public class EFDataAccess : IDataAccess
{
public string GetName(int id)
{
// return a string from db using EF
}
}
现在您的服务 class 将是
public class SendService : ISendService
{
private IDataAccess dataAccess;
public SendService(IDataAccess dataAccess)
{
this.dataAccess=dataAccess;
}
// to do : Implement methods of your ISendService interface.
// you may use this.dataAccess in those methods as needed.
}
在您的单元测试中,您可以创建接口的模拟实现,其中 returns 静态数据而不是访问数据库。
例如,如果您使用的是 Moq 模拟框架,则可以这样做。
var m = new Mock<IDataAccess>();
var m.Setup(s=>s.GetName(It.IsAny<int>())).Returns("Test");
var s = new SendService(m);
var result= s.SomeMethod();
我可以让它工作,但我想知道最佳做法是什么以及为什么。我有一个控制器、一个模型和一个存储库,现在我想对控制器进行单元测试。我只是在编写一个简单的测试以确保返回正确的视图。
这是我在控制器中的方法:
public ActionResult Selections(SelectionsViewModel model)
{
for (int i = 0; i < model.Sends.Count; i++)
{
Send send = new Send(new SendService(new Database().GetConnectionString()))
{
SendID = model.Sends[i].SendID,
Title = model.Sends[i].Title,
Subject = model.Sends[i].Subject,
SentDate = model.Sends[i].SentDate,
TimesViewed = model.Sends[i].TimesViewed,
Include = model.Sends[i].Include,
Exclude = model.Sends[i].Exclude
};
send.UpdateIncludeExclude();
}
return View(model);
}
这是数据库 class 中我的 GetConnectionString() 方法,它通过我的 SendService 构造函数发送。
public string GetConnectionString()
{
return System.Configuration.ConfigurationManager.ConnectionStrings["DEVConnectionString"].ToString();
}
最后,我的单元测试:
[Test]
public void TestAssignmentSelections()
{
var obj = new AssignmentController();
var actResult = obj.Selections() as ViewResult;
NUnit.Framework.Assert.That(actResult.ViewName, Is.EqualTo("Selections"));
}
现在,我的单元测试失败了,我知道原因了。我的单元测试项目无权访问我正在测试的项目的 web.config 我的连接字符串所在的位置。
我做了一些研究,显然只要在我的单元测试项目中添加一个 web.config 并将连接字符串也放在那里就可以使它工作..但这似乎是一个 hack。
解决此问题的最佳方法是什么?有没有另一种方法来编写我的代码来适应这个?
您想使您的控制器单元可测试吗?不要这样做。
new SendService(
使用此代码,您将对具体服务实现和数据访问代码实现进行硬编码。在您的单元测试中,您不应该真正访问数据库中的数据。相反,您应该提供一个模拟数据访问实现。
接口来了,你需要为你的SendService
创建一个接口。
public interface ISendService
{
void SomeMethod();
}
现在你的SendService
将是这个接口的具体实现
public class SendService : ISendService
{
public void SomeMethod()
{
// Do something
}
}
现在更新您的控制器以拥有一个构造函数,我们将在其中注入 ISendService
.
public class YourController : Controller
{
private ISendService sendService;
public YourController(ISendService sendService)
{
this.sendService = sendService;
}
public ActionResult YourActionMethod()
{
// use this.sendService.SomeMethod();
}
}
并且您可能会使用一些依赖注入框架来告诉 MVC 框架在代码运行时使用哪个接口实现。如果你使用的是 MVC6,它有一个你可以使用的内置依赖注入提供程序。因此,转到 Startup
class 并在 ConfigureServices
方法中,您可以将接口映射到具体实现。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISendService, SendService>();
}
}
如果您使用的是以前版本的 MVC,您可以考虑像 Unity 这样的 DI 框架,Ninject 等。您可以对稍后的数据访问/服务层执行相同的方法。即:创建一个用于数据访问的接口并将其注入您的 SendService。
public Interface IDataAccess
{
string GetName(int id);
}
和一个使用您的特定数据访问的实现 code/ORM
public class EFDataAccess : IDataAccess
{
public string GetName(int id)
{
// return a string from db using EF
}
}
现在您的服务 class 将是
public class SendService : ISendService
{
private IDataAccess dataAccess;
public SendService(IDataAccess dataAccess)
{
this.dataAccess=dataAccess;
}
// to do : Implement methods of your ISendService interface.
// you may use this.dataAccess in those methods as needed.
}
在您的单元测试中,您可以创建接口的模拟实现,其中 returns 静态数据而不是访问数据库。
例如,如果您使用的是 Moq 模拟框架,则可以这样做。
var m = new Mock<IDataAccess>();
var m.Setup(s=>s.GetName(It.IsAny<int>())).Returns("Test");
var s = new SendService(m);
var result= s.SomeMethod();