18 января 2012

Mock-объект для текстового файла в Python

Достаточно часто встречается ситуация, когда какой-нибудь библиотечный файл требует в качестве параметра имя текстового файла или соответствующий файловый объект. Рассмотрим, например, задачу разбора ini-файла стандартной структуры: сначала идет имя секции в квадратных скобках, затем список параметров и их значений. В Python эта задача решается с помощью модуля стандартной библиотеки ConfigParser. Код выглядит приблизительно так:

def getPropertyA(configFile):
Config = ConfigParser.ConfigParser()
Config.readfp(open(configFile, 'r'))
return Config.get('A', 'a')
Данный пример откроет файл с именем configFile и передаст его файловый дескриптор объекту класса ConfigParser. Метод readfp() выполнит разбор файла, а метод get() вернет, в данном случае, свойство с именем a из секции A.
Но, перед тем как написать подобный код, практика программирования требует от нас реализовать соответствующий модульный тест. Тут возникает следующая проблема: тест зависит от содержимого ini-файла, если оно изменится — тест сломается. Понятно, что вместо тестового файла можно было бы подставить его текстовое содержимое, но тут возникает следующая сложность. Класс ConfigParser имеет два метода для инициализации парсера: read() и readfp(). Первый получает в качестве параметра имя файла и, очевидно, нам не подходит, а второй — объект, реализующий функцию readline(). Для работы со строками, как с файловыми объектами, в библиотеке Python есть модуль StringIO:
test = StringIO.StringIO("[A]\na = 1\nb = 2")
Переменная test является mock-объектом, для которого реализованы функции файлового ввода/вывода, т.е. ее можно передать в качестве параметра в readfp(). Остается еще одна проблема: в приведенном выше коде для получения файлового объекта применяется функция open(). Понятно, что с объектом test она работать не будет. Решить эту проблему можно, благодаря тому, что в Python функции являются объектами первого класса. Зададим в функции парсинга дополнительный параметр по умолчанию:
def getPropertyA(configFile, open = open):
Config = ConfigParser.ConfigParser()
Config.readfp(open(configFile, 'r'))
return Config.get('A', 'a')
В данном случае, локальная переменная open получает в качестве значения функцию open() из глобального пространства имен, которая, собственно, предназначена для открытия файла. Если второй параметр не задан, используется значение по умолчанию, и функция корректно работает с именем файла в качестве первого параметра. В случае же, когда мы передаем mock-объект, функцию open() надо переопределить так, чтобы она просто возвращала то, что получила на входе. Тестовый случай будет выглядеть так:
self.assertEqual(getPropertyA(test, open = lambda s, t: s), "1")
Анонимная функция получает два параметра, для имени файла и режима открытия, а возвращает только первый.

Комментариев нет: