如何进行模拟测试?(涉及IT技术)

13
在一个.NET的Windows应用程序中,我有一个名为EmployeeManager的类。在实例化时,该类从数据库中加载未完成注册的员工到一个列表中。我想在单元测试中使用EmployeeManager。然而,我不想涉及数据库。
根据我的理解,我需要一个IEmployeeManager接口,只用于测试目的。这似乎不太对,因为这个接口没有其他用处。但是,它可以让我创建一些EmployeeManager测试类,以便在不涉及数据库的情况下加载员工。这样,我就可以分配那些原本来自数据库的值。
上述是否正确?我需要进行模拟吗?模拟(Moq框架)似乎需要大量的代码才能完成简单的任务,如分配属性。我不明白这个意义所在。既然我可以从IEmployeeManager创建一个简单的测试类来提供我需要的东西,为什么还要进行模拟呢?

5
指着它笑?[跑开并躲起来] - Marc B
5个回答

10
控制反转是你的解决方案,而不是模拟对象。原因如下:
你会模拟接口以确保使用IEmployeeManager的某些代码是否正确。你没有使用测试代码来证明IEmployeeManager有效。因此,必须有另一个类,它接受IEmployeeManager,例如,您将使用模拟对象进行实际测试的类。
如果您实际上只是测试EmployeeManager,那么您可以做得更好。考虑依赖注入。通过这种方式,您将为EmployeeManager公开一个构造函数,该构造函数将至少需要一个接口参数。您的EmployeeManager代码将在内部使用此接口进行任何需要进行的实现特定调用。
参见策略模式
这将引导您进入整个令人兴奋的控制反转世界。随着您深入研究,您会发现像AutoFacNinjectStructure Map这样的IoC容器已经有效地解决了这些问题。
Mocking接口很好,你可以mock一个接口,然后将其传递给IoC。但是你会发现,IoC是解决问题的更加强大的方案。是的,虽然你可能只是为了测试而实现第二个替代方案,但出于这个原因,将被测试的策略与EmployeeManager的业务逻辑分离仍然非常重要。

1
在这种情况下,我不明白为什么需要IoC。看起来像是一个解决方案在寻找问题。 - 4thSpace
1
在你的情况下,策略模式就足够了--你不需要一个IoC框架。然而,副作用是通过将接口传递到构造函数(或提供公共属性),你的策略可以很好地融入IoC模式中。重点是,为了能够在测试代码中模拟实现,重写类以从接口派生是错误的。在那一点上,你真正测试的只是你是否正确编写了模拟设置。 - Michael Hays

5
从我对这种情况的理解来看,我需要一个IEmployeeManager接口,该接口仅用于测试目的。这似乎不正确,因为该接口没有其他用途。
值得创建该接口。请注意,该接口实际上具有多个目的:
  1. 接口标识由参与者提供的角色或职责。 在这种情况下,接口标识了EmployeeManager的角色和职责。使用接口可以防止意外依赖于特定于数据库的内容。
  2. 接口减少耦合。 由于应用程序不会依赖于EmployeeManager,因此您可以自由地替换其实现,而无需重新编译其余部分的应用程序。当然,这取决于项目结构、程序集数量等,但它仍然允许这种重用。
  3. 接口促进可测试性。 使用接口后,生成动态代理以更轻松地对软件进行测试变得更加容易。
  4. 接口强制思考1。 好吧,我已经暗示过了,但值得再说一遍。仅使用接口就应该使您考虑一个对象的角色和职责。接口不应成为一个厨房水槽。接口表示一组凝聚的角色和职责。如果接口方法不是凝聚的或几乎总是一起使用,则可能一个对象具有多个角色。虽然不一定是坏事,但它意味着多个不同的接口更好。接口越大,使其在代码中具有协变性或逆变性就越难,因此越不易塑形。
然而,这将允许我创建一些EmployeeManager测试类,加载员工而不涉及数据库... 我不明白重点在哪里。为什么要模拟,当我可以只创建一个简单的从IEmployeeManager继承的测试类,提供我所需的内容呢?
正如一位发帖者指出的那样,你似乎在谈论创建存根测试类。模拟框架可用于创建存根,但它们最重要的特点之一是它们允许您测试行为而不是状态。现在让我们看一些例子。假设以下情况:
interface IEmployeeManager {
    void AddEmployee(ProspectiveEmployee e);
    void RemoveEmployee(Employee e);
}

class HiringOfficer {
    private readonly IEmployeeManager manager
    public HiringOfficer(IEmployeeManager manager) {
        this.manager = manager;
    }
    public void HireProspect(ProspectiveEmployee e) {
        manager.AddEmployee(e);
    }
}

当我们测试HiringOfficerHireEmployee行为时,我们感兴趣的是验证他是否正确地向员工经理传达了这位潜在雇员应该被添加为员工的信息。通常会看到类似以下的内容:
// you have an interface IEmployeeManager and a stub class
// called TestableEmployeeManager that implements IEmployeeManager
// that is pre-populated with test data
[Test]
public void HiringOfficerAddsProspectiveEmployeeToDatabase() {
    var manager = new TestableEmployeeManager(); // Arrange
    var officer = new HiringOfficer(manager); // BTW: poor example of real-world DI
    var prospect = CreateProspect();
    Assert.AreEqual(4, manager.EmployeeCount());

    officer.HireProspect(prospect); // Act

    Assert.AreEqual(5, manager.EmployeeCount()); // Assert
    Assert.AreEqual("John", manager.Employees[4].FirstName);
    Assert.AreEqual("Doe", manager.Employees[4].LastName);
    //...
}

上面的测试是合理的...但不够好。它是一种基于状态的测试。也就是说,通过检查某些操作之前和之后的状态来验证行为。有时这是测试事物的唯一方法;有时这是测试某些东西的最佳方法。
但是,测试行为通常更好,这就是模拟框架发挥作用的地方:
// using Moq for mocking
[Test]
public void HiringOfficerCommunicatesAdditionOfNewEmployee() {
    var mockEmployeeManager = new Mock<EmployeeManager>(); // Arrange
    var officer = new HiringOfficer(mockEmployeeManager.Object);
    var prospect = CreateProspect();

    officer.HireProspect(prospect); // Act

    mockEmployeeManager.Verify(m => m.AddEmployee(prospect), Times.Once); // Assert
}

在上面的测试中,我们测试了唯一真正重要的事情——招聘官向员工经理传达需要添加新员工的信息(仅一次……尽管在这种情况下我实际上不会费心检查计数)。不仅如此,我还验证了我要求招聘官雇用的员工已由员工经理添加。我测试了关键行为。我甚至不需要一个简单的测试存根。我的测试更短。实际行为更加明显——可以看到对象之间的交互并验证交互。
虽然可能让你的存根测试类记录交互,但这样你就是在模拟框架。如果你要测试行为——使用模拟框架。
作为另一位发帖者提到的,依赖注入(DI)和控制反转(IoC)非常重要。我上面的例子并不是这方面的好例子,但这两个概念应该仔细考虑并谨慎使用。关于这个主题有很多文章可以参考阅读 1 - 是的,思考仍然是可选的,但我强烈建议这样做 ;)。

1
在您的情况下提取接口是一个好主意。我不会太担心您只需要这个进行测试的事实。提取此接口使您的代码与数据库解耦。之后,您可以选择编写自己的实现进行测试,或使用模拟框架为您生成此实现。这是个人喜好的问题。这取决于您对模拟框架的熟悉程度以及是否想花时间学习新的语法。
在我看来,值得学习。它将节省您大量的打字时间。它们也很灵活,不总是需要接口来生成测试实现。例如,RhinoMocks可以模拟具有空构造函数和方法为虚拟的具体类。另一个优点是模拟API使用一致的命名,因此您将熟悉“Mocks”、“Stubs”等。顺便说一下,在您的情况下,您需要存根,而不是模拟。手动编写实际模拟可能比使用框架更费力。

使用模拟框架的危险在于,其中一些框架非常强大,可以模拟几乎任何东西,包括私有字段(TypeMock)。换句话说,它们对设计错误过于宽容,允许您编写非常耦合的代码。

这是一个关于手写与生成存根的好阅读材料


1

创建一个 IEmployeeManager 接口以便能够进行模拟,这是大多数 .NET 开发人员进行使这样的类可测试的方式。

另一个选项是从 EmployeeManager 继承并覆盖要测试的方法,以便它不涉及数据库 - 这也意味着您需要更改设计。


谢谢。如果我使用IEmployeeManager并创建一个针对它的测试类,那么如何测试已经在EmployeeManager中实现的方法的功能?此外,在这种情况下,模拟什么时候变得有用? - 4thSpace
@4thSpace - 你需要分别测试它们。接口的目的是使使用EmployeeManager类的类可测试。 - Oded
2
如果你想测试EmployeeManager的实现,你需要模拟数据库。 - Mike Zboray
@mikez,你有没有使用Unity Moq框架的示例? - 4thSpace
我没有使用Unity的经验,但这个链接看起来很合理:https://dev59.com/d2865IYBdhLWcg3wfemU - Mike Zboray

0
通过让你的类实现接口,你不仅使它们更具可测试性,也使你的应用程序更具灵活性和可维护性。当你说“这并不正确,因为接口没有其他用处”时,这是错误的,因为它允许你将类松散耦合。
如果我可以建议几本书《Head First 设计模式》《Head First 软件开发》会更好地解释这些概念,比我在 SO 中的回答更好。
如果你不想使用像 Moq 这样的模拟框架,那么编写自己的模拟对象/存根就很简单了,这里有一个关于如何做到这一点的博客文章Rolling your own Mock Objects

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