在.NET中对服务层进行单元测试服务

7

我正在开发一个新的MVC 5应用程序。 我在项目中有一个集成层,其中我有一个对外部Web服务的服务引用。

然后,我创建了一个接口,其中包含一些方法 - 然后我有一个服务类,类似于下面的代码:

public class MyService : IMyService
{
    private readonly ExternalServiceClient _serviceClient;

    public MyService()
    {
        _serviceClient = new ExternalServiceClient ("WSHttpBinding_IMyService");
    }

    public string GetName(Coordinate[] coordinates)
    {
        string name = string.Empty;

        name = _serviceClient.GetInformationsForCoordinates(coordinates);

        return name;
    }

请注意,这是简化版的内容,实际上将添加try catch块和异常处理等。
我有一个Integration.Tests项目,用于集成层的单元测试。测试GetName方法的最佳实践方法是什么?我应该在Tests项目中向服务端点添加另一个Service Reference,还是如果我们使用Moq,我可以创建一个实际服务的实例,例如Web层将调用它 - 那该怎么做呢?

1
顺便说一下,你的服务似乎没有做太多事情。也许最好从ExternalServiceClient中提取一个接口,并直接将其作为依赖项传递。 - MikeSW
2个回答

13

针对您的情况,最佳做法在于架构设计。

你的服务显然依赖于ExternalServiceClient,并且你将其实例化在MyService类内部,这样不容易切换依赖项,使得在测试时会给你带来很多麻烦。

真正的问题应该是:

我应该如何设计我的服务,以便于它可以轻松进行测试?

答案就是依赖注入 (Dependency Injection)。

因为你将能够模拟MyService的依赖项,你将能够全面地测试它,并通过红绿重构来证明你的服务工作得非常出色。

在我看来,你的类应该像这样:

public class MyService : IMyService {
    public MyService(ExternalServiceClient serviceClient) {
        externalServiceClient = serviceclient;
    }

    public string GetName(Coordinate[] coordinates) {
        string name = string.Empty;
        name = externalServiceClient.GetInformationForCoordinates(coordinates);
        return name;
    }

    private readonly ExternalServiceClient externalServiceclient;
}

这样,你就可以随意替换你的依赖关系,因此使用模拟对象。

使用 NUnitMoq,你可以按照以下方式测试你的服务:

[TestFixture]
public class MyServiceTests {
    [TestFixture]
    public class GetCoordinates : MyServiceTests {
        // Given
        string expected = "Name";
        Coordinate[] coordinates = new Coordinate[] { ... }
        externalServiceClientMock.Setup(esc => esc.GetInformationForCoordinates(coordinates)).Returns(expected);

        // When
        string actual = myService.GetName(coordinates);

        // Then
        externalServiceClientMock.Verify(esc => esc.GetInformationCoordinates(coordinates));
        Assert.AreEqual(expected, actual);
    }

    [SetUp]
    public void MyServiceSetup() {
        externalServiceClientMock = new Mock<ExternalServiceClient>("WSHttpBinding_IMyService");
        myService = new MyService(externalServiceClientMock.Object);
    }
    
    private Mock<ExternalServiceClient> externalServiceClientMock;
    private MyService myService;
}

构造函数注入是首选方式。请查看我的编辑。 - Will Marcouiller
使用 DI 工具,您甚至可以使用一些“条件绑定”将 ExternalServiceClient 依赖注入到 string 原始类型中。我不知道 SimpleInjector。真正的问题是在您的类中没有任何对 DI 内核、容器或其他名称的引用。DI 注入工具应该仅在您的“组合根”(即程序入口点)中被引用。 - Will Marcouiller
1
构造函数注入对于API用户来说更加清晰,比基于注解的注入更易于文档化,并且更简单易测。无论哪种类型的依赖注入都比隐藏依赖好,但是为了测试目的,我更喜欢在构造函数中显式传递它们。 - ssube
唯一我在你的回答中没有看到的是在ExternalServiceClient中指定("WSHttpBinding_IMyService")的地方。 - Ctrl_Alt_Defeat
此外,在测试中使用 DI 注入工具会使新手更难理解您的测试。在这种情况下,让新关键字也许是一个好方法来说明意图。此外,如果您可以在 Web 应用程序和单元测试中使用相同的组合根,则每次运行任一项时都必须更改类型注册。这对我来说听起来很麻烦。因此,我更喜欢使用我的答案中提供的代码示例,或者使用特定于您的测试的组合根基类。 - Will Marcouiller
显示剩余7条评论

1
通常情况下,最好的做法是(由于您在ExternalServiceClient中具有readonly语义),要有参数化构造函数以便能够在对象构造时注入依赖项。然后您可以在测试用例中注入模拟对象。
在您的情况下,如果存在在ExternalServiceClient中实现的接口,则需要注意。
private readonly ExternalServiceClient _serviceClient;

public MyService(IExternalServiceClient serviceClient)
{
    _serviceClient = serviceClient;
}

然后按如下方式使用它:
var service = new MyService(new ExternalServiceClient ("WSHttpBinding_IMyService"));

和测试相关

IExternalServiceClient  mockObject = //construct mock with desired behabiour then pass to ctor
var service = new MyService(mockObject);

如果没有实现接口或者能力添加它(因为它是外部的),你必须使用虚拟性进行一些技巧。


哈姆雷特 - 所以这行代码 var service = new MyService(new ExternalServiceClient ("WSHttpBinding_IMyService")); 其中 WSHttp 被实例化 - 这将是每个方法的第一行,例如 public string GetName(Coordinate[] coordinates)。 - Ctrl_Alt_Defeat
@KOL 这是一个问题吗? - Hamlet Hakobyan
是的,那是一个问题 - 每个方法都需要这个新的吗? - Ctrl_Alt_Defeat
每当您需要创建对象(MyService)或重新创建上下文时,都需要使用new关键字。 - Hamlet Hakobyan
依赖注入工具,如Ninject、SimpleInjection、StructureMap等,用于避免每次编写new This(new That(new Here(new There()))) - Will Marcouiller
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接