如何模拟 WCF 服务






4.64/5 (10投票s)
在编写单元测试时如何模拟 WCF 服务。
引言
单元测试现在已成为任何构建中不可或缺的一部分。 单元测试通常应该为最小的代码单元编写,即执行某些特定且独立工作的各种方法。 但是,如果您的方法调用不同的层(例如方法 => 业务逻辑层 => 数据层)来完成其工作,并且您想为此编写单元测试怎么办? 如果您的单元测试实际上也调用所有这些层,那么它与单元测试的实际目的并不相符。 单元测试肯定不应该调用其他层。 当您想为内部调用 WCF 服务的方法编写单元测试时,情况会变得有点棘手和困难。
为了克服这个问题,我们使用 Mock 框架。 市场上有很多 Mock 框架,但我将在这里讨论使用 MOQ 进行模拟。 让我们以 WCF 场景为例,并尝试在使用 MOQ 模拟框架模拟 WCF 客户端方法(调用 WCF 服务)的单元测试时,模拟一个 WCF 方法。
我也会在本文后面重点介绍其他一些功能。
Using the Code
我的 WCF 服务 `WCFService` 派生自接口 `IWCFService`,并具有一个方法 `GetData(int value)`
namespace WCFService { public class WCFService : IWCFService { public string GetData(int value) { return string.Format("You entered: {0}", value); } } }
WCF 客户端 `WCFClient` 使用服务代理 `WCFServiceAgent` 消耗 `WCFService`。 服务代理实现一个方法 `HitWCFService`()
,该方法调用服务方法 `GetData()`
namespace WCFClient { public class WCFServiceAgent { IWCFService wcfService; public WCFServiceAgent() { this.wcfService = new WCFServiceClient(); } // constructor for unit test public WCFServiceAgent(bool isUnitTest) { this.wcfService = UnityHelper.IoC.Resolve<IWCFService>(); } public string HitWCFService() { int val = 1; string retVal = string.Empty; // call wcf service retVal = this.wcfService.GetData(val); return retVal; } } }
现在,我们想为 `HitWCFService()` 编写一个单元测试。 为了避免实际的服务调用,我们需要模拟 `wcfService.GetData(val)` 方法。
namespace WCFClientTest { [TestClass] public class WCFClientUnitTest { [TestMethod] public void MockWCFService() { string val = "MockedValue";string actualRetVal; // mock wcf interface Mock<IWCFService>wcfMock = new Mock<IWCFService>(); // setup for wcf service GetData method wcfMock.Setup<string>(s=> s.GetData(It.IsAny<int>())).Returns(val); // create object to register with IoC IWCFService wcfMockObject = wcfMock.Object; // register instance UnityHelper.IoC = new UnityContainer(); UnityHelper.IoC.RegisterInstance<IWCFService>(wcfMockObject); // create ServiceAgent object using parameterized constructor (for unit test) WCFServiceAgent serviceAgent = new WCFServiceAgent(true); // method call to be tested actualRetVal = serviceAgent.HitWCFService(); // verify if the expected method called during test or not wcfMock.Verify(s => s.GetData(It.IsAny<int>()), Times.Exactly(1)); Assert.AreEqual("MockedValue", actualRetVal, "Not same."); } } }
在上面编写的示例中,我们首先创建 `IWCFService` 接口的模拟对象。 `Mock<IWCFService> wcfMock = new Mock<IWCFService>();`
只是为了记住,不能为具体类创建模拟对象,即不可能编写类似的内容
Mock<WCFService> wcfMock = new Mock<WCFService>().wcfMock.Setup<string>(s => s.GetData(It.IsAny<int>())).Returns(val);
使用上面指定的代码行,我们告诉模拟框架我们希望模拟 GetData 方法,并且返回值将是 `val`。 换句话说,当调用 `GetData()` 时,模拟这个方法被调用并返回 `val`。
现在,我们将 `wcfMockObject` 的实例注册到 IoC,这会将 IoC 绑定到在遇到语句 `IoC.Resolve()` 时返回相同的对象。
// register instance UnityHelper.IoC = new UnityContainer(); UnityHelper.IoC.RegisterInstance<IWCFService>(wcfMockObject);我通过一个小技巧进一步处理了这种情况。 我使用参数化构造函数在服务代理中创建服务客户端对象。 如果您查看代码,则使用简单的构造函数创建 ServiceAgent 对象(当实际应用程序创建该对象时),但对于单元测试,我使用了参数化构造函数,其中 IoC.Resolve 为我提供了注册的模拟对象。
// constructor for unit test public WCFServiceAgent(bool isUnitTest) { this.wcfService = UnityHelper.IoC.Resolve<IWCFService>(); }供您参考,`UnityHelper` 只是如下所示
namespace WCFClient { public static class UnityHelper { public static UnityContainer IoC; } }现在,我的单元测试方法的倒数第二行验证了是否发生了预期的方法调用。
// verify if the expected method called during test or not wcfMock.Verify(s => s.GetData(It.IsAny<int>()),Times.Exactly(1));这行可以写成
wcfMock.Verify(s => s.GetData(4), Times.Exactly(1));
但随后您的设置方法也应该如此
wcfMock.Setup<string>(s => s.GetData(4)).Returns(val);
但是,如果传递给模拟方法的参数是在我们为其编写单元测试的方法内部创建或初始化的,则这会产生问题。 我故意在 `HitWCFService` 内部创建了参数 val (val = 1)。 模拟框架将这两种方法视为不同的方法 (GetData(4) 和 GetData(1) 不同),当您验证它时,它会抛出一个错误,内容类似:“预期一次但从未调用”。
如果您不希望 MOQ 比较参数,请使用 It.IsAny<int>()。 这会导致 MOQ 在期望模拟方法时忽略参数。
更多信息,请参阅演示应用程序。
- Amit Shrivastava, 系统分析师, Avanade/Accenture (Gurgaon, India)