Ответы на экзамен (987689), страница 15
Текст из файла (страница 15)
Класс должен быть протестирован по всем перечисленным случаям.
В идеальном случае необходимо повести исчерпывающее тестирование класса: работа класса должна проверяться на всех возможных значениях, чтобы убедиться в его правильной работе. На практике исчерпывающее тестирование может требовать недопустимо больших затрат, поэтому обычно применяют достаточное тестирование. Наиболее распространенными критериями достаточности покрытия методов класса тестами являются:
1. Покрытие, основанное на ограничениях: сколько пар предусловие – постусловие покрываются тестами.
2. Покрытие, ориентированное на состояния: основано на том, сколько переходов между состояниями покрываются тестами.
3. Покрытие на основе программных кодов: все ли пути графа управления программы пройдены хоты бы один раз (разумеется, могут существовать пути, пройти которые невозможно в принципе).
На практике рекомендуют поступить следующим образом: составить тесты по одному из перечисленных критериев и проверить, необходимо ли добавить тесты для удовлетворения критериям достаточности по другим критериям. Как правило, критерии применяют в той последовательности, как они были перечислены выше.
51. Тестирование взаимодействия классов. Тестирование иерархии классов.
1) Тестирование взаимодействия классов.
Основное назначение тестирования взаимодействия состоит в том, чтобы убедиться в правильности обмена сообщениями между объектами. Взаимодействие объектов представляет собой запрос одного объекта на выполнение другим объектом одной из своих операций. Кроме того, широко используются взаимодействия, когда классу требуются объекты других классов для выполнения своих операций:
∙••••••• Один класс может быть типом формального параметра метода другого класса.
∙••••••• Один класс может быть типом возвращаемого значения метода другого класса.
∙••••••• Метод одного класса создает экземпляр другого класса, как часть своей реализации.
Общим признаком перечисленных случаев является то, что один класс обслуживает структуру данных для другого класса: обеспечивает хранение данных со сложной структурой и операции над ними. Типичным примером такого использования классов являются контейнерные классы в языках С++ и С#. Такое взаимодействие классов называют коллекцией. Тестирование коллекций проводится аналогично тестированию примитивных классов, описанному в предыдущем параграфе. Тестовый драйвер должен обеспечить выполнение всех заложенных в коллекции операций.
Тестирование взаимодействия двух объектов является достаточно простой задачей. Количество потенциальных взаимодействующих объектов быстро становится значительным, и многие серьёзные ошибки возникают именно при совместном использовании некоторого множества объектов. При этом возникает вопрос: как тестировать каждое взаимодействие – индивидуально или в рамках некоторой группы взаимодействий? Правильный выбор «куска» для тестирования имеет большое практическое значение для успеха процесса в целом.
При составлении тестов для проверки взаимодействия классов необходимо учитывать, какой подход был применен при разработке классов: контрактный или защитный. При контрактном подходе ответственность за выполнение предварительных условий операций возлагается на отправителя. Поэтому тестированием должно быть установлено, что ни один класс не отправляет сообщений, не удовлетворяющих предусловиям методов класса – получателя. В классе получателе такие проверки тогда не нужны. Если правильность предусловий не может полностью быть проверена в классе отправителе (например, класс получатель должен часть необходимых для выполнения операции данных получить из своей базы данных), то для таких операций может быть применен и принцип защитного программирования.
При применении защитного программирования необходимо тестами проверить допустимость постусловий и выполнение реакций методов на невыполнение постусловий.
Для эффективного построения тестов взаимодействия могут быть использованы диаграммы взаимодействия. Напомним, что одна диаграмма взаимодействия соответствует реализации одного варианта использования по одному сценарию. Отсюда следует, что для достаточного тестирования взаимодействия классов необходимо таким образом подбирать тесты, чтобы все возможные сценарии были реализованы. Дополнительно для проверки достаточности тестирования можно использовать приведенные выше критерии достаточности тестирования одного отдельно взятого класса (см. предыдущий параграф).
2) Тестирование иерархии классов.
Тестирование иерархии классов всегда выполняется «сверху - вниз». Допустим, что класс С является наследником класса А и класс А прошел тестирование классов и взаимодействия классов.
С может отличаться от А по следующим признакам:
∙••••••• В С добавлены новые операции в интерфейс класса и, возможно, новые методы для их реализации.
∙••••••• В С изменена спецификация и/или реализация операции из А.
∙••••••• С содержит новые переменные по сравнению с А.
∙••••••• Инвариант класса С может отличаться от инварианта А.
Поскольку С наследует часть своей спецификации от А, то все тесты, ориентированные на тестирование A, могут быть задействованы и при тестировании С. Для проверки новых операций необходимо разработать и новые тесты. Если инварианты в подчиненном классе уточнены, то могут потребоваться и тесты для их проверки.
Инкрементные изменения, внесенные в класс А во время построения производного класса В, могут быть использованы при выявлении, что потребуется тестировать в В. Унаследованными тестовыми случаями называют тесты, предназначенные для тестирования базового класса и используемые для тестирования класса-наследника. Путем несложного анализа можно выяснить, какие унаследованные тестовые случаи могут быть использованы при тестировании наследника и какие нет.
Рассмотрим эти случаи подробнее.
В С добавлены новые операции в интерфейс класса и, возможно, новые методы для их реализации. В таком случае новые операции привносят новые функциональные возможности и новые программные коды, которые должны быть протестированы.
В С изменена спецификация операции из А. Требуются новые тесты для проверки измененных операций. Новые тесты могут быть составлены согласно рассмотренным выше принципам.
В С перекрыт метод из А. Для него можно повторно использовать все унаследованные тестовые случаи. В связи с появлением нового программного кода потребуется пересмотр всех тестов для их всесторонней проверки.
С содержит новые переменные по сравнению с А. Новая переменная, скорее всего, появилась вместе с изменениями в методах. Поэтому тестирование новых (перекрытых) методов проверяет и работу с этой переменой.
Инвариант класса С может отличаться от инварианта А. Инвариант класса можно рассматривать как дополнительное постусловие. Результат теста подвергается ограничениям со стороны инвариантов. Следовательно, если инвариант менялся, требуется повторный прогон всех унаследованных тестов, чтобы убедиться в выполнении новых инвариантов.
Нет необходимости добавлять тесты, ориентированные на спецификации, которые остались неизменными при переходе от базового класса к наследнику. Тесты могут быть повторно использованы в первозданном виде. Их повторный прогон не требуется, если методы, для проверки которых они предназначались, не изменились. Потребуется повторный прогон теста, если метод изменился косвенным путем (например, использует измененный метод). В таком случае могут потребоваться и дополнительные тесты для проверки измененных операций.
Для практической организации тестирования класса-наследника можно построить тестовый драйвер в виде класса-наследника тестового драйвера базового класса.
При тестировании абстрактных классов возникает принципиальная трудность, потому что их объекты не могут быть созданы. Используют следующие подходы:
1. создается класс-наследник с заглушкой чисто виртуального (С++) или абстрактного метода (Delphi, C#);
2. абстрактный класс тестируется как часть прямого потомка;
3. разрабатывается специальная реализация абстрактного класса для тестирования.
67.Ламбда-выражения и их использование на LINQ
Простейший запрос состоит из одной входной последовательности и одного оператора. Например, мы можем применить оператор where к строковому массиву и извлечь те его элементы, длина которых не меньше четырех символов. Поскольку стандартные операторы запроса реализованы в виде методов расширения, мы можем вызвать Where непосредственно ДЛЯ массива names так, словно это метод экземпляра:
IEnumerable<string> filteredNames = names."Where (n => n.Length >= 4);
Большинство операторов запроса принимает лямбда-выражение в качестве аргумента. Лямбда-выражение помогает направить и сформировать запрос. В нашем примере лямбда-выражение выглядит так:
n => п.Length >= 4
Входной аргумент соответствует входному элементу. В этом случае входной аргумент n представляет имя в массиве и имеет тип string. Оператор where требует, чтобы лямбда-выражение возвращало значение типа bool. Когда оно истинно, элемент должен быть включен в выходную последовательность.
Будем называть такие запросы лямбда-запросами
Чтобы строить более сложные запросы, вы добавляете новые операторы, образуя цепочку. Например, в следующем запросе из массива извлекаются все строки с буквой "а", после чего они сортируются по длине и переводятся в верхний регистр:
string[] names = {"Tom","Dick","Harry","Mary","Jay" }; IEnumerable<string> query = names.Where (n => n.Contains ("a")) .OrderBy (n => n.Length) .Select (n => n.ToUpper());
foreach (string name in query)
Console.Write (name + "|");
Результат: JAY|MARY|HARRY|
Примечание
Оператор запроса никогда не изменяет входную последовательность. Вместо нее он возвращает новую. Это соответствует парадигме функционального программирования, из которой исходит LINQ.
Когда операторы запроса выстраиваются в цепочку, как в нашем примере, выходная последовательность одного оператора становится входной для следующего.
В предыдущих примерах мы передавали оператору where такое лямбда-выражение:
n => n.Contains ("а") // На входе строка,
// на выходе булево выражение
Предназначение лямбда-выражения зависит от конкретного оператора запроса. С оператором where оно показывает, должен ли элемент попадать в выходную последовательность. У оператора OrderBy лямбда-выражение отображает каждый элемент входной последовательности на ключ сортировки, а у оператора select оно определяет, как должен быть преобразован элемент из входной последовательности перед подачей его в выходную.
Примечание
В операторе запроса лямбда-выражение всегда относится к отдельным элементам входной последовательности, а не к последовательности в целом.
Вы можете относиться к лямбда-выражению как к обратному вызову. Оператор запроса вычисляет значение лямбда-выражения "по требованию",— как правило, один раз для каждого элемента входной последовательности. Лямбда-выражения позволяют вам внести свою логику в операторы запроса. Это делает операторы запроса гибкими и в то же время простыми по внутреннему устройству. Приведем полную реализацию метода Enumerable.Where, опустив то, что касается обработки исключений:
public static IEnumerable<TSource> Where<TSource> (
this IEnumerable<TSource> source,
Func<TSource,bool> predicate)
{
foreach (TSource element in source)
if (predicate (element))
yield return element;
}
Лямбда-выражения и типы элементов
В стандартных операторах запроса используются следующие имена обобщенных типов.
Обозначение обобщенного типа | Смысл |
TSource | Тип элемента входной последовательности |
TResult | Тип элемента выходной последовательности — отличается от TSource |
TKey | Тип ключа, используемого при сортировке, группировании или объединении |
Тип TSource определяется входной последовательностью. Типы TResult и Tkey выводятся из вашего лямбда-выражения.