Как проверить, что метод вызывается в тестируемом классе? - PullRequest
9 голосов
/ 11 марта 2012

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

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

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

потому что тестируемый класс не подделка. Кроме того, если тестируемый метод вызывает другие методы в тестируемом классе, которые, в свою очередь, также вызывают методы в том же классе, мне понадобится подделать кучу значений только для проверки метода «верхнего уровня». Поскольку я также провожу модульное тестирование всех этих «под-методов», я могу предположить, что «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 ]

6 голосов
/ 11 марта 2012

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

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

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

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

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

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

2 голосов
/ 11 марта 2012

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

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

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

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

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

1 голос
/ 11 марта 2012

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

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

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

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

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

0 голосов
/ 11 марта 2012

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

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

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

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

public DataSet ExportExcelDocToDataSet (bool headerRowProvided, DataTable dtExcelData)

Таким образом, вам не нужно будет смоделировать метод GetDataTableFromExcelSheet внутри метода ExportExcelDocToDataSetTet * DataTet * DataSata1et1Set1et1Set1et1Set1 *.

...