0

首先让我声明,尽管我是一个相当新的 TDD 实践者,但我对它的好处非常满意。我觉得我已经取得了足够的进步,可以考虑使用模拟,并且在了解模拟在 OOP 中的位置时遇到了真正的障碍。

我已经阅读了尽可能多的关于该主题的相关帖子/文章(FowlerMiller),但仍然不完全清楚如何或何时嘲笑。

让我举一个具体的例子。我的应用程序有一个服务层类(有人称它为应用程序层?),其中方法大致映射到特定用例。这些类可以与持久层、域层甚至其他服务类协作。我一直是一个很好的 DI 小男孩,并且已经正确地分解了我的依赖项,因此可以将它们替换为测试目的等。

示例服务类可能如下所示:

public class AddDocumentEventService : IAddDocumentEventService
{
    public IDocumentDao DocumentDao
    {
        get { return _documentDao; }
        set { _documentDao = value; }
    }
    public IPatientSnapshotService PatientSnapshotService
    {
        get { return _patientSnapshotService; }
        set { _patientSnapshotService = value; }
    }

    public TransactionResponse AddEvent(EventSection eventSection)
    {
        TransactionResponse response = new TransactionResponse();
        response.Successful = false;

        if (eventSection.IsValid(response.ValidationErrors))
        {

            DocumentDao.SaveNewEvent( eventSection,  docDataID);

            int patientAccountId = DocumentDao.GetPatientAccountIdForDocument(docDataID);
            int patientSnapshotId =PatientSnapshotService.SaveEventSnapshot(patientAccountId, eventSection.EventId);

            if (patientSnapshotId == 0)
            {
                throw new Exception("Unable to save Patient Snapshot!");
            }

            response.Successful = true;
        }
        return response;
    }

}

我通过使用 NMock 隔离了它的依赖项(DocumentDao、PatientSnapshotService)来测试这个方法的过程。这是测试的样子

 [Test]
 public void AddEvent()
    {
        Mockery mocks = new Mockery();
        IAddDocumentEventService service = new AddDocumentEventService();
        IDocumentDao mockDocumentDao = mocks.NewMock<IDocumentDao>();
        IPatientSnapshotService mockPatientSnapshot = mocks.NewMock<IPatientSnapshotService>();

        EventSection eventSection = new EventSection();

        //set up our mock expectations
        Expect.Once.On(mockDocumentDao).Method("GetPatientAccountIdForDocument").WithAnyArguments();
        Expect.Once.On(mockPatientSnapshot).Method("SaveEventSnapshot").WithAnyArguments();
        Expect.Once.On(mockDocumentDao).Method("SaveNewEvent").WithAnyArguments();

        //pass in our mocks as dependencies to the class under test
        ((AddDocumentEventService)service).DocumentDao = mockDocumentDao;
        ((AddDocumentEventService)service).PatientSnapshotService = mockPatientSnapshot;

        //call the method under test
        service.AddEvent(eventSection);

        //verify that all expectations have been met
        mocks.VerifyAllExpectationsHaveBeenMet();
    }

我对这个嘲弄的小尝试的想法如下:

  1. 这个测试似乎打破了许多基本的 OO 规则,尤其是封装:我的测试完全了解被测类的具体实现细节(即方法调用)。每当类内部发生变化时,我都会看到很多非生产性的时间花在更新测试上。
  2. 也许是因为我的服务类目前相当简单,但我不太清楚这些测试增加了什么价值。我是否保证正在按照特定用例的要求调用协作对象?对于这么小的好处,代码重复似乎高得离谱。

我错过了什么?

4

6 回答 6

2

你提到了马丁福勒关于这个主题的一篇非常好的帖子。他提到的一点是,模仿者是喜欢测试行为并孤立事物的人。

经典的 TDD 风格是尽可能使用真实的对象,如果使用真实的东西很尴尬,则使用 double。所以经典的 TDDer 会使用真实的仓库和用于邮件服务的 double。double 的类型并不重要那么多。

然而,mockist TDD 从业者总是对任何具有有趣行为的对象使用mock。在这种情况下,仓库和邮件服务都适用。"

如果你不喜欢这种东西,你可能是一个经典的 TDDer,并且应该只在尴尬的时候使用 mocks(比如邮件服务,或者信用卡收费)。否则,您将创建自己的替身(如创建内存数据库)。

特别是,我是一个模仿者,但我不会验证是否正在调用特定方法(除非它不返回值)。无论如何,我都会测试接口。当函数返回一些东西时,我使用模拟框架来创建存根。

最后,这一切都取决于您想要测试的内容和方式。您认为检查这些方法是否真的被调用(使用模拟)很重要吗?您是否只想检查通话前后的状态(使用假货)?选择足以认为它正在工作的东西,然后构建你的测试来准确地检查它!

关于测试的价值,我有一些看法:

  • 在短期内,当您进行 TDD 时,您通常会获得更好的设计,尽管您可能需要更长的时间。
  • 从长远来看,您以后不会太害怕更改和维护此代码(当您记不太清楚细节时),您会立即获得红点,几乎是即时反馈

顺便说一句,测试代码大小与生产代码大小一样大是正常的。

于 2009-08-31T18:13:47.180 回答
1

打破封装从而使您的测试更紧密地耦合到您的代码绝对是使用模拟的缺点。您不希望您的测试在重构时变得脆弱。这是一条你必须走的细线。我个人避免使用模拟,除非它真的很难、笨拙或很慢。查看您的代码,首先,我将使用 BDD 样式:您的测试方法应该测试该方法的特定行为并且应该这样命名(可能类似于 AddEventShouldSaveASnapshot)。其次,经验法则是只验证预期的行为是否发生,而不是对每个应该发生的方法调用进行分类。

于 2009-08-31T18:35:12.627 回答
1

使用模拟进行的测试应该可以帮助您理解协作对象之间的关系——描述它们应该如何相互通信的协议。在这种情况下,您想知道当事件到达时有几件事会被持久化。如果您要描述对象的外部关系,则不会破坏封装。DAO 和 Service 类型描述了这些外部服务,但没有定义它们是如何实现的。

这只是一个小例子,但代码感觉是程序性的,而不是面向对象的。有几种情况是从一个对象中提取简单值并传递给另一个对象。也许某种 Patient 对象应该直接处理该事件。很难说,但也许测试暗示了这些设计问题?

同时,如果你不介意自我推销,可以再等一个月, http://www.growth-object-oriented-software.com/

于 2009-10-04T23:07:51.087 回答
1

当我写这样的测试时,我有同样的不安感觉。当我通过将期望复制'n'粘贴到函数体中来实现该函数时,尤其令我印象深刻(当我使用 LeMock 进行模拟时,这很有效)。

但没关系。它发生了。该测试现在记录并验证被测系统如何与其依赖项交互,这是一件好事。但是,此测试还有其他问题:

  1. 它一次测试太多。此测试验证是否正确调用了三个依赖项。如果这些依赖项中的任何一个发生更改,则此测试将不得不更改。最好有 3 个单独的测试,验证每个依赖项都得到了正确处理。为您未测试的依赖项传入一个存根对象(而不是模拟,因为它会失败)。

  2. 没有对传递给依赖项的参数进行验证,因此这些测试是不完整的。

在这个示例中,我将使用 Moq 作为模拟库。这个测试没有指定所有依赖的行为,它只测试一个调用。它还将检查传入的参数是否符合输入的预期,输入的变化将证明单独的测试是合理的。

public void AddEventSavesSnapshot(object eventSnaphot)
{
    Mock<IDocumentDao> mockDocumentDao = new Mock<IDocumentDao>();
    Mock<IPatientSnapshotService> mockPatientSnapshot = new Mock<IPatientSnapshotService>();

    string eventSample = Some.String();
    EventSection eventSection = new EventSection(eventSample);

    mockPatientSnapshot.Setup(r => r.SaveEventSnapshot(eventSample));

    AddDocumentEventService sut = new AddDocumentEventService();
    sut.DocumentDao = mockDocumentDao;
    sut.PatientSnapshotService = mockPatientSnapshot;

    sut.AddEvent(eventSection);

    mockPatientSnapshot.Verify();
}

请注意,只有在 AddEvent() 可能使用它们的情况下,才需要在此测试中传入未使用的依赖项。相当合理地,该类可能具有与未显示的此测试无关的依赖项。

于 2010-01-04T19:56:50.887 回答
0

我发现为这些类型的测试提供一个假的(内存中的)持久层很有用;然后,您可以验证最终结果(项目现在是否存在于存储库中),而不是验证是否进行了某些调用。我知道你正在使用模拟,但我想我是说我认为这不是最好的地方。

例如,这个测试的伪代码我会看到:

Instantiate the fake repositories.
Run your test method.
Check the fake repository to see if the new elements exist in it.

这使您的测试不知道实现细节。但是,这确实意味着维护虚假的持久层,所以我认为这是您必须考虑的权衡。

于 2009-08-31T18:24:14.943 回答
0

有时值得将代码中的利益相关者分开。

封装是关于最小化具有最高更改传播概率的潜在依赖项的数量。这是基于静态源代码的。

单元测试是关于确保运行时行为不会无意中改变:它不是基于静态源代码。

当单元测试人员不是针对原始的、封装的源代码,而是针对源代码的副本工作时,它有时很有用,它的所有私有访问器都自动更改为公共访问器(这只是一个四行 shell 脚本)。

这清楚地将封装与单元测试分开。

然后只剩下你在单元测试中走多低:你想测试多少方法。这是一个品味问题。

有关封装的更多信息(但没有关于单元测试),请参阅: http ://www.edmundkirwan.com/encap/overview/paper7.html

于 2009-09-01T17:32:56.187 回答