Еще один вопрос насмешки - PullRequest
       35

Еще один вопрос насмешки

0 голосов
/ 31 августа 2009

Прежде всего позвольте мне заявить, что, несмотря на то, что я довольно новый практикующий специалист по TDD, я в значительной степени продан его преимуществам. Я чувствую, что уже достаточно продвинулся, чтобы подумать об использовании насмешек, и наткнулся на настоящую кирпичную стену, когда дело доходит до понимания, где насмешки сочетаются с ООП.

Я прочитал столько соответствующих постов / статей по этому вопросу, сколько смог найти ( Фаулер , Миллер ), и до сих пор не совсем ясно, как и когда издеваться.

Позвольте мне привести конкретный пример. В моем приложении есть класс сервисного уровня (некоторые называют его прикладным уровнем?), В котором методы примерно соответствуют конкретным случаям использования. Эти классы могут взаимодействовать с постоянным уровнем, уровнем домена и даже с другими классами обслуживания. Я был хорошим маленьким мальчиком 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;
    }

}

Я прошел процесс тестирования этого метода в изоляции его зависимостей (DocumentDao, PatientSnapshotService) с помощью NMock. Вот как выглядит тест

 [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. Этот тест, по-видимому, нарушает многие фундаментальные предписания ОО, не в последнюю очередь из которых является инкапсуляция: мой тест полностью осведомлен о конкретных деталях реализации тестируемого класса (т.е. вызовы методов). Я вижу много непродуктивного времени, затрачиваемого на обновление тестов при изменении внутренних классов.
  2. Возможно, это потому, что мои классы обслуживания на данный момент довольно упрощены, но я не совсем понимаю, какую ценность эти тесты добавляют. Неужели я гарантирую, что взаимодействующие объекты вызываются в зависимости от конкретного варианта использования? Дублирование кода кажется нелепо высоким для такой маленькой выгоды.

Чего мне не хватает?

Ответы [ 6 ]

2 голосов
/ 31 августа 2009

Вы упомянули очень хороший пост от Мартина Фаулера на эту тему. Одна вещь, которую он упоминает, состоит в том, что mockists - те, кто любит проверять поведение и изолировать вещи.

" Классический стиль TDD состоит в том, чтобы использовать реальные объекты, если это возможно, и двойные, если неудобно использовать реальные вещи. Так что классический TDDer будет использовать реальный склад и двойную для почтовой службы. двойное не имеет большого значения.

Практикующий-теоретик TDD, тем не менее, всегда будет использовать макет для любого объекта с интересным поведением. В этом случае как для склада, так и для почтовой службы."

Если вам не нравятся подобные вещи, вы, вероятно, классический TDDer, и должны использовать макеты только тогда, когда это неудобно (например, почтовый сервис или оплата кредитной картой). В противном случае вы создаете свои собственные двойники (например, при создании базы данных в памяти).

В частности, я издеватель, но я мало проверяю, вызывается ли конкретный метод (если он не возвращает значения). В любом случае, я бы тестировал интерфейсы. Когда функция возвращает что-то, я использую макет для создания заглушек.

В конце концов, все сводится к тому, что и как вы хотите проверить . Как вы думаете, важно проверить, действительно ли эти методы были вызваны (используйте mocks)? Хотите просто проверить состояние до и после звонка (использовать подделки)? Выберите то, что достаточно, чтобы считать его работоспособным, а затем создайте свои тесты, чтобы точно это проверить!

По поводу стоимости тестов у меня есть несколько мнений:

  • В краткосрочной перспективе когда вы используете TDD, вы обычно получаете лучший дизайн , хотя это может занять больше времени.
  • В долгосрочной перспективе вы не будете слишком бояться изменить и сохранить этот код позже (когда вы не очень хорошо запомните детали), и вы сразу же получите красный, почти мгновенная обратная связь .

Кстати, размер тестового кода обычно такой же, как размер рабочего кода.

1 голос
/ 04 января 2010

У меня такое же неприятное чувство, когда я пишу подобные тесты. Это особенно поражает, когда я реализую функцию путем копирования и вставки ожиданий в тело функции (это работает, когда я использую 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 () может их использовать. Вполне разумно, что у класса могут быть зависимости, не связанные с этим тестом, который не показан.

1 голос
/ 05 октября 2009

Тест с имитациями должен помочь вам понять отношения между взаимодействующими объектами - протокол, который описывает, как они должны общаться друг с другом. В этом случае вы хотите знать, что пара вещей будет сохранена при наступлении события. Это не нарушает инкапсуляцию, если вы описываете внешние отношения объекта. Типы DAO и Service описывают эти внешние службы, но не определяют, как они реализованы.

Это лишь небольшой пример, но код выглядит скорее процедурным, чем ОО. Есть несколько случаев, когда простые значения извлекаются из одного объекта и передаются другому. Возможно, какой-то объект Patient должен обрабатывать событие напрямую. Трудно сказать, но, может быть, тест намекает на эти проблемы дизайна?

А пока, если вы не против саморекламы и можете подождать еще месяц, http://www.growing -object-oriented-software.com /

1 голос
/ 31 августа 2009

Нарушение инкапсуляции и, как следствие, более тесная связь ваших тестов с вашим кодом, безусловно, может быть недостатком использования имитаторов. Вы не хотите, чтобы ваши тесты были хрупкими против рефакторинга. Это тонкая грань, по которой вы должны идти. Лично я избегаю использования издевательств, если это не очень сложно, неловко или медленно. Глядя на ваш код, во-первых, я бы использовал стиль BDD: ваш тестовый метод должен проверять определенное поведение метода и должен называться таковым (возможно, что-то вроде AddEventShouldSaveASnapshot). Во-вторых, эмпирическое правило заключается только в том, чтобы проверять, произошло ли ожидаемое поведение, а не в каталог каждого отдельного вызова метода, который должен был произойти.

0 голосов
/ 01 сентября 2009

Иногда стоит разделить заинтересованные стороны в вашем коде.

Инкапсуляция - это минимизация количества потенциальных зависимостей с наибольшей вероятностью распространения изменений. Это основано на статическом исходном коде.

Модульное тестирование означает, что поведение во время выполнения не изменяется непреднамеренно: оно не основано на статическом исходном коде.

Иногда полезно, когда тестеры модулей работают не с оригинальным инкапсулированным исходным кодом, а с копией исходного кода, в которой все его частные средства доступа автоматически заменены на открытые средства доступа (это всего лишь сценарий оболочки из четырех строк ).

Это четко отделяет инкапсуляцию от модульного тестирования.

Остается только то, как низко вы идете в своем модульном тестировании: сколько методов вы хотите протестировать. Это дело вкуса.

Подробнее об инкапсуляции (но ничего о модульном тестировании) смотрите: http://www.edmundkirwan.com/encap/overview/paper7.html

0 голосов
/ 31 августа 2009

Я считаю полезным иметь поддельный (в памяти) слой персистентности для этих типов тестов; Затем вместо проверки того, что были сделаны определенные вызовы, вы можете проверить конечный результат (что элементы теперь существуют в хранилище). Я знаю, что вы работаете над использованием насмешек, но, полагаю, я говорю, что не думаю, что это лучшее место, чтобы это делать.

В качестве примера псевдокода для этого теста я бы увидел:

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

Это позволяет вашим тестам не знать особенностей реализации. Тем не менее, это означает сохранение слоя поддельной персистентности, поэтому я полагаю, что это компромисс, который вы должны рассмотреть.

...