2012-03-11 4 views
8

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

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

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

ответ

6

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

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

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

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

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

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

+0

Я подумал об извлечении методов, которые вы упомянули, в отдельные классы, чтобы я мог тестировать их с помощью расширения AssertWasCalled, но я не был уверен, что это отличная идея, потому что я мог видеть, что это приводит к тонне очень мелких классов с один или два метода в них не более. Я обязательно просмотрю рекомендованную вами книгу, а также больше о SRP. Самое лучшее во всем этом - я единственный человек в проекте, и весь код написан мной, поэтому я могу делать все, что мне нужно, чтобы он работал. –

+1

@Gage Trader: не измеряйте класс по количеству методов. Например, Command Pattern предоставляет только один метод и по-прежнему является используемым методом. Прежде чем я применил модульное тестирование и хорошие принципы проектирования OO, мне не нравилось большее количество классов. Я взял время, чтобы понять, что большее число классов не означает больше работы. Да, это правда, что вам нужно больше времени для разработки и тестирования, но более высокое качество и более низкий показатель ошибок компенсируют это на сегодняшний день. –

1

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

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

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

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

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

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

+0

У меня есть конструктор, который принимает объект IWorkbook, чтобы издеваться над объектом рабочей книги (я не показывал никаких конструкторов в моем примере), поэтому эта часть позаботилась. GetDataTableFromExcelSheet был бы закрыт, если бы я не хотел его тестировать. Я вижу, что вы говорите о тестировании только публичных методов - я открывал много своих ранее приватных методов для тестирования из-за сложности некоторых методов на более высоких уровнях. Мне очень нравятся ваши советы, потому что я тратил много времени на тесты для каждого метода, который у меня был, что, вероятно, не подходит для этого. –

2

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

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

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

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

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

+0

Хорошее предположение о названии класса: оно называется «ExcelHelper», и идея заключается в том, чтобы он был особым классом, называемым остальной частью моего приложения, чтобы иметь дело со всем, что связано с Excel. Я должен создать хотя бы класс ExcelImporter и ExcelExporter, и после прочтения этих комментариев у меня есть некоторые идеи для дальнейшего разделения кода. Я всегда думал о рефакторинге с точки зрения разделения кода на более мелкие частные методы в том же классе, чтобы обрабатывать работу, а не писать больше классов с определенными целями. –

+1

@GageTrader: * помощники *, * менеджеры * и все, что смутно названные классы обычно указывают на нарушение SRP. «ExcelImporter» очень хорошо связывает свою роль/цель. Что означает «ExcelHelper»? Помогает с чем ...? Сложность с присвоением классов является одним из признаков нарушения SRP - сложнее найти подходящее имя для класса. Вы должны определенно реорганизовать часть своего кода - ваш дизайн будет более чистым, понятным и более легко проверяемым. –

0

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

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

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

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

общественного DataSet ExportExcelDocToDataSet (BOOL headerRowProvided, DataTable dtExcelData)

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

+0

Хороший вопрос о передаче DataTable вместо того, чтобы просто получить возвращаемое значение. Я склоняюсь к flip-flop о том, как это сделать: передайте экземпляр объекта или верните его вызывающему методу. У меня все еще больше зависимостей ниже стека вызовов (не показано), хотя у меня все еще будет оригинальная проблема в том, что вы не можете проверить некоторые из этих методов нижнего уровня. –

Смежные вопросы