Как проверить, что метод был вызван в тестируемом классе?

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

я смог протестировать большую часть своего кода, используя NUnit и Rhino mocks без особых проблем. Однако мне было интересно узнать о функциях модульного тестирования, которые в конечном итоге вызывают много других методов в том же классе. Я не могу сделать что-то подобное

classUnderTest.AssertWasCalled(cut => cut.SomeMethod(someArgs))

потому что тестируемый класс не является подделкой. Кроме того, если метод, который я тестирую, вызывает другие методы в тестируемом классе, которые, в свою очередь, также вызывают методы в том же классе, мне нужно будет подделать тонну значений, чтобы проверить метод "верхнего уровня". Поскольку я также тестирую все эти "методы sub", я должен иметь возможность предположить, что "SomeMethod" работает так, как ожидалось, если он проходит модульный тест и не нужно беспокоиться о деталях этих более низких уровней методы.

вот пример кода, с которым я работал, чтобы проиллюстрировать свою точку зрения (я написал класс для управления импортом / экспортом файлов Excel с помощью NPOI):

    public DataSet ExportExcelDocToDataSet(bool headerRowProvided)
    {
        DataSet ds = new DataSet();

        for (int i = 0; i < currentWorkbook.NumberOfSheets; i++)
        {               
            ISheet tmpSheet = currentWorkbook.GetSheetAt(i);

            if (tmpSheet.PhysicalNumberOfRows == 0) { continue; }
            DataTable dt = GetDataTableFromExcelSheet(headerRowProvided, ds, tmpSheet);

            if (dt.Rows.Count > 0)
            {
                AddNonEmptyTableToDataSet(ds, dt);
            }
        }

        return ds;
    }

    public DataTable GetDataTableFromExcelSheet(bool headerRowProvided, DataSet ds, ISheet tmpSheet)
    {
        DataTable dt = new DataTable();
        for (int sheetRowIndex = 0; sheetRowIndex <= tmpSheet.LastRowNum; sheetRowIndex++)
        {
            DataRow dataRow = GetDataRowFromExcelRow(dt, tmpSheet, headerRowProvided, sheetRowIndex);
            if (dataRow != null && dataRow.ItemArray.Count<object>(obj => obj != DBNull.Value) > 0)
            {
                dt.Rows.Add(dataRow);
            }
        }

        return dt;
    }

...

видно, что ExportExcelDocToDataSet (мой метод "верхнего уровня" в этом случае) вызывает GetDataTableFromExcelSheet которых звонки GetDataRowFromExcelRow, который вызывает несколько других методов, которые определены в этом же самом класс.

Итак, какова рекомендуемая стратегия рефакторинга этого кода, чтобы сделать его более тестируемым без необходимости заглушать значения, вызываемые подметодами? Есть ли способ подделать вызовы методов в тестируемом классе?

заранее спасибо за любую помощь или совет!

4 ответов


изменить тему под тестом (SUT). Если что-то трудно тестировать, то дизайн может быть неудобным.

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

во избежание над определенными испытаниями, сконцентрируйте на публике методы. Если этот метод вызывает другие методы в классе, не тестируйте эти вызовы. С другой стороны: метод вызывает other зависит от компонента (DOCs) должны быть проверены.

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

в вашем конкретном случае вы можете сделать следующее: извлеките методов ExportExcelDocToDataSet и GetDataTableFromExcelSheet в два разных класса (возможно, назовите их ExcelToDataSetExporter и ExcelSheetToDataTableExporter). Исходный класс, содержащий оба метода, должен ссылаться на оба класса и вызывать эти методы, который вы ранее извлекли. Теперь вы можете тестировать все три класса в изоляции. Применить рефакторинг класса извлечения (книги) для достижения модификации исходного класса.

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


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

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

Теперь, чтобы дать вам несколько советов:

  • как называется ваш тестируемый класс? Основываясь на методах, которые он раскрывает, что-то вроде ExcelExporterAndToDataSetConverter ... или ExcelManager? Кажется, что этот класс может делать слишком много вещей сразу; это требует немного рефакторинга. Экспорт данных в DataSet можно легко отделить от преобразования данных excel в наборы данных / DataRows.
  • что случается, когда GetDataTableFromExcelSheet изменения способа? Перемещается в другой класс или заменяется сторонним кодом? Должен ли он нарушить ваши экспортные тесты? Это не должно - это одна из причин, по которой ваши экспортные тесты не должны проверять, был ли он вызван или нет.

Я предлагаю перейти к методам преобразования DataSet/DataRow в отдельный класс - это облегчит написание модульных тестов, и ваши тесты экспорта не будут такими хрупкими.


Я думаю, вы тестируете публичный метод GetDataTableFromExcelSheet отдельно, поэтому для испытаний ExportExcelDocToDataSet вам не нужно проверять поведение GetDataTableFromExcelSheet (помимо того факта, что ExportExcelDocToDataSet работает, как ожидалось).

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

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

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

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

в этом случае, хотя очевидное изменение было бы для ExportExcelDocToDataSet взять книгу в качестве аргумента. В тестировании вы можете отправить поддельную книгу. См.инверсия управления.


одно можно сказать наверняка, что вы делаете TDD правильно :) Ну, в приведенном выше коде вам придется издеваться над методом GetDataTableFromExcelSheet перед тестированием метода ExportExcelDocToDataSet.

но вместо этого вы можете передать datatable, возвращенный из GetDataTableFromExcelSheet из места в вашем коде, где вы вызвали метод ExportExcelDocToDataSet, добавив другой параметр.

что-то вроде этого

DataTable dtExcelData = метод GetData....; и измените метод, как показано ниже

открытый набор данных ExportExcelDocToDataSet (bool headerRowProvided, DataTable dtExcelData)

таким образом, вам не нужно будет макет GetDataTableFromExcelSheet внутри метода ExportExcelDocToDataSet при тестировании метода ExportExcelDocToDataSet.