使用 Unity 依赖注入模拟 MVC 控制器中的外部 WCF 服务





5.00/5 (4投票s)
在 MVC 控制器中模拟(moq)您的服务。
引言
下载WcfUnity.zip (VS2013)
为了便于打包,我已从zip下载中删除了所有NuGet包和bin dll。但是,当您编译项目时,解决方案将从NuGet下载所有相应的程序集。我使用的是Visual Studio 2013,但您可以使用VS2012 Web Express及更高版本。
背景
在单元测试控制器方法时,您需要模拟代码与之通信的层。这可以使用RhinoMock或MOQ等模拟框架轻松完成。第一层和最后一层永远无法模拟,因为您无法控制UI(第一层)或外部服务或数据库过程(最后一层)。对于这些场景,您将使用Selenium或CodedUI等集成测试工具来执行端到端测试(从UI到服务\数据库)。
但是,在某些情况下,您需要模拟倒数第二层,但您对其的控制仍然有限,因为它可能部署在服务器上,而您的应用程序正在使用它——例如,一个与外部数据库通信或执行某些数字计算逻辑的SOAP服务。在单元测试中,您只关心控制器方法中的逻辑,而不是服务——因此,您希望模拟服务调用,因为服务在部署之前已经过单元测试。
IOC & 项目结构
对于我的IOC容器,我将使用MVC.Unity 4 Nuget包。我的测试项目分为三个项目:
- WCF服务项目
- MVC Web应用程序
- 测试项目
代码解释
为了简单起见,我在Global.asax类中的Application_Start方法中设置了Unity解析器(更好的方法是创建一个静态的BootStraper方法并从Application_Start调用它——使代码更易于维护——因为在普通应用程序中,这个方法最终会变得代码拥挤)。
在添加Unity解析器代码之前,您需要将外部服务引入您的项目。在我的例子中,我将StockServices
部署到我的本地IIS,然后像往常一样将WSDL引入我的项目。
protected void Application_Start()
{
var container = new UnityContainer();
container
.RegisterType<IStock, StockClient>()
.Configure<InjectedMembers>()
.ConfigureInjectionFor<StockClient>(new InjectionConstructor("*"));
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
上面代码片段中,将接口解析为具体类的代码非常标准。唯一有趣的是,我从哪里获取服务名称——StockCleint
?
如果您在项目中双击服务引用,将打开**对象浏览器**窗口,您可以向下滚动到服务条目——服务名称将始终带有Client
后缀。这就是您希望服务接口解析到的具体类名。
现在您知道在哪里以及如何解析服务依赖项。接下来的部分是如何将服务注入我的控制器,然后在*构造函数*注入后执行服务方法。
Controller 代码
在下面的代码片段中,有几行代码值得我们关注。 namely,我们接口IStock
的构造函数注入和私有变量stock
。 只有从MVC 3开始,才有可能创建一个带有构造函数的控制器类。因此,我们可以使用Unity的构造函数依赖注入方法,通过构造函数实现我们的具体类。
现在将服务注入控制器非常容易,调用服务方法也直观——但需要注意的重要一点是,您正在注入您的接口,因此稍后可以在测试中模拟您的接口。为了简单起见,我只是调用服务来返回一个数据类型和一个类结构。
public class HomeController : Controller
{
private readonly StockService.IStock stock;
public HomeController(StockService.IStock stk)
{
stock = stk;
}
public ActionResult Index()
{
ViewBag.Message = stock.GetData(10);
return View("Index");
}
public ActionResult About()
{
StockService.StockData data = stock.GetStockData();
ViewBag.Message = String.Format("Your stock {0} has a value {1}.", data.StockName, data.StockValue);
return View("About", data);
}
}
创建测试
最困难的部分已经完成,因为我们只需像平常一样创建单元测试,模拟服务及其方法。我们将模拟服务注入我们正在测试的控制器构造函数,然后像平常一样测试控制器方法。在下面的测试中,我只是测试了模拟的模型或ViewBag,但您可能希望使用MVC助手来测试生成的HTML等。
[TestClass]
public class HomeControllerTest
{
[TestMethod]
public void Index()
{
// test variables
string expectedViewName = "Index";
string returnValue = "300";
// mocking objects
Mock<IStock> mockIStock = new Mock<IStock>();
mockIStock.Setup(t => t.GetData(It.IsAny<int>())).Returns(returnValue);
// Arrange
HomeController controller = new HomeController(mockIStock.Object);
// Act
ViewResult result = controller.Index() as ViewResult;
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(result.ViewBag.message, returnValue);
Assert.AreEqual(expectedViewName, result.ViewName, "View name should have been {0}", expectedViewName);
}
[TestMethod]
public void About()
{
// test variables
string expectedViewName = "About";
string stockName = "BON";
double stockValue = 1;
// mocking objects
Mock<StockData> mockStockData = new Mock<StockData>();
mockStockData.Object.StockName = stockName;
mockStockData.Object.StockValue = stockValue; // penny stock :)
Mock<IStock> mockIStock = new Mock<IStock>();
mockIStock.Setup(t => t.GetStockData()).Returns(mockStockData.Object);
// Arrange
HomeController controller = new HomeController(mockIStock.Object);
// Act
ViewResult result = controller.About() as ViewResult;
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.ViewBag.message == String.Format("Your stock {0} has a value {1}.", stockName, stockValue));
Assert.AreEqual(expectedViewName, result.ViewName, "View name should have been {0}", expectedViewName);
Assert.AreEqual(result.Model, mockStockData.Object, "Model should have been {0} and {1}", mockStockData.Object.StockName, mockStockData.Object.StockValue);
}
}
运行测试
要在VS2012\13中运行测试,只需右键单击测试类并从上下文菜单中选择**运行测试**,这将显示您的测试结果——您可以插入断点并像往常一样调试您的测试。
结论
模拟外部服务非常容易,在处理 动态构建的服务终结点时,需要多一个步骤——但同样,在惰性加载服务时,您只需使用终结点详细信息(例如,WSHttpBinding_IStock
)作为您的解析具体类。