2008-08-27 5 views
124

Я слышал, что модульное тестирование «абсолютно потрясающее», «действительно круто» и «всевозможные хорошие вещи», но 70% или более моих файлов связаны с доступом к базе данных (некоторые прочитайте, а некоторые напишите), и я не уверен, как написать модульный тест для этих файлов.Как выполнить модульный тест объекта с запросами базы данных

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

ответ

68

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

Для того, чтобы настроить свои объекты для насмехаясь, вы, вероятно, нужно использовать какой-то инверсии управления/зависимостей шаблона впрыска, как показано в следующем псевдокоде:

class Bar 
{ 
    private FooDataProvider _dataProvider; 

    public instantiate(FooDataProvider dataProvider) { 
     _dataProvider = dataProvider; 
    } 

    public getAllFoos() { 
     // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction 
     return _dataProvider.GetAllFoos(); 
    } 
} 

class FooDataProvider 
{ 
    public Foo[] GetAllFoos() { 
     return Foo.GetAll(); 
    } 
} 

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

class BarTests 
{ 
    public TestGetAllFoos() { 
     // here we set up our mock FooDataProvider 
     mockRepository = MockingFramework.new() 
     mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider); 

     // create a new array of Foo objects 
     testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()} 

     // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos, 
     // instead of calling to the database and returning whatever is in there 
     // ExpectCallTo and Returns are methods provided by our imaginary mocking framework 
     ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray) 

     // now begins our actual unit test 
     testBar = new Bar(mockFooDataProvider) 
     baz = testBar.GetAllFoos() 

     // baz should now equal the testFooArray object we created earlier 
     Assert.AreEqual(3, baz.length) 
    } 
} 

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

23

В идеале, ваши объекты должны быть неуверенными. Например, у вас должен быть «уровень доступа к данным», к которому вы должны обращаться, чтобы возвращать объекты. Таким образом, вы можете оставить эту часть из своих модульных тестов или проверить их изолированно.

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

В моих проектах C# я использую NHibernate с полностью отдельным слоем данных. Мои объекты живут в модели основного домена и доступны из моего прикладного уровня. Уровень приложения говорит как об уровне данных, так и о слое модели домена.

Слой приложения также иногда называют «бизнес-слоем».

Если вы используете PHP, создайте определенный набор классов для ТОЛЬКО доступ к данным. Убедитесь, что ваши объекты не имеют представления о том, как они сохраняются, и подключите их в своих классах приложений.

Другой вариант - использовать насмешливые/заглушки.

+0

Я всегда соглашался с этим, но на практике из-за крайних сроков и «хорошо, теперь добавьте еще одну особенность, к сегодняшнему 14:00» это одно из самых сложных достижений. Тем не менее, этот вопрос является главной целью рефакторинга, хотя мой босс когда-либо решает, что он не думал о 50 новых возникающих проблемах, требующих совершенно новой бизнес-логики и таблиц. – 2015-07-23 18:50:27

+1

Если ваши объекты плотно связаны с вашим слоем данных, трудно выполнить надлежащее модульное тестирование. первая часть модульного теста - «единица». Все устройства должны быть протестированы изолированно. хорошее объяснение – 2015-08-09 09:33:26

6

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

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

2

Обычно я пытаюсь разбить свои тесты между тестированием объектов (и ORM, если таковые имеются) и тестированием дБ. Я тестирую объектную сторону вещей, издеваясь над вызовами доступа к данным, в то время как я тестирую сторону db вещей, проверяя взаимодействие объектов с db, которое, по моему опыту, обычно довольно ограничено.

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

2

Вы можете использовать mocking frameworks, чтобы абстрагироваться от базы данных. Я не знаю, получили ли PHP/Python некоторые из них, но для типизированных языков (C#, Java и т. Д.) Есть много вариантов.

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

2

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

Простую форму IoC можно выполнить только путем кодирования на интерфейсы. Для этого требуется некоторая ориентация объектов в вашем коде, чтобы она не распространялась на то, что вы делаете (я говорю, что, поскольку все, что я должен продолжить, это ваше упоминание о PHP и Python)

Надеюсь, что это полезно, если ничего иначе у вас есть некоторые условия для поиска.

2

Я согласен с первым доступом к базе данных, который должен быть удален в слой DAO, который реализует интерфейс. Затем вы можете протестировать свою логику против реализации заглушки DAO.

4

В книге xUnit Test Patterns описаны способы обработки кода модульного тестирования, который попадает в базу данных. Я согласен с другими людьми, которые говорят, что вы не хотите этого делать, потому что это медленно, но вы должны сделать это когда-нибудь, ИМО. Издевательствовать соединение db для тестирования материала более высокого уровня - хорошая идея, но ознакомьтесь с этой книгой за предложениями о том, что вы можете сделать, чтобы взаимодействовать с реальной базой данных.

2

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

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

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

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

Извините, у меня нет каких-либо конкретных примеров кода для PHP/Python, но если вы хотите увидеть .NET-пример, у меня есть post, который описывает технику, которую я использовал для того же самого теста.

4

Вариантов у вас есть:

  • Написать скрипт, который будет уничтожить базу данных, прежде чем начать модульные тесты, а затем заполнить БД с заранее определенным набором данных и выполнением тестов. Вы также можете сделать это до каждого теста –, это будет медленным, но менее подверженным ошибкам.
  • Внесите базу данных. (Пример в псевдо-Java, но применим ко всем OO-языкам)

     
    class Database { 
    public Result query(String query) {... real db here ...} 
    }

    class MockDatabase extends Database { public Result query(String query) { return "mock result"; } }

    class ObjectThatUsesDB { public ObjectThatUsesDB(Database db) { this.database = db; } }

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

  • Не используйте БД вообще на протяжении большей части кода (в любом случае это плохая практика). Создайте объект «базы данных», который вместо возвращения с результатами вернет нормальные объекты (т.е. вернет User вместо кортежа {name: "marcin", password: "blah"}) напишите все ваши тесты с специальными сконструированными объектами real и напишите один большой тест, который зависит от базы данных, которая убедитесь, что это преобразование работает нормально.

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

11

Простейший способ модульного тестирования объекта с доступом к базе данных - использовать области транзакций.

Например:

[Test] 
    [ExpectedException(typeof(NotFoundException))] 
    public void DeleteAttendee() { 

     using(TransactionScope scope = new TransactionScope()) { 
      Attendee anAttendee = Attendee.Get(3); 
      anAttendee.Delete(); 
      anAttendee.Save(); 

      //Try reloading. Instance should have been deleted. 
      Attendee deletedAttendee = Attendee.Get(3); 
     } 
    } 

Это вернется состояние базы данных, в основном, как откат транзакции, так что вы можете запустить тест столько раз, сколько вы хотите без каких-либо побочных эффектов. Мы успешно использовали этот подход в крупных проектах. Наша сборка занимает немного времени, чтобы работать (15 минут), но это не ужасно, если у вас 1800 единиц тестов. Кроме того, если время сборки вызывает беспокойство, вы можете изменить процесс сборки, чтобы иметь несколько сборок, один для сборки src, другой, который запускается после этого, который обрабатывает модульные тесты, анализ кода, упаковку и т. Д.

+0

+1 Сохраняет много времени при модульном тестировании слоев доступа к данным. Просто обратите внимание, что для TS часто требуется MSDTC, что может быть нежелательно (в зависимости от того, будет ли ваше приложение понадобиться MSDTC) – StuartLC 2012-05-26 07:22:52

6

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

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

После того, как это было на месте, мы тогда были в состоянии сделать что-то подобное в нашем коде (мы работаем в C++, но я уверен, что вы получите идею):.

GetDatabase() ExecuteSQL (" INSERT INTO foo (blah, blah) ")

В обычном режиме работы GetDatabase() возвращает объект, который передал все наши sql (включая запросы), через ODBC напрямую в базу данных.

Затем мы начали изучать базы данных в памяти - лучший путь длинным образом кажется SQLite. (http://www.sqlite.org/index.html). Это очень просто настроить и использовать, и позволило нам подкласс и переопределить GetDatabase() для пересылки sql в базу данных в памяти, которая была создана и уничтожена для каждого проведенного теста.

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

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

Очевидно, что наш опыт сосредоточен вокруг среды разработки на C++, однако я уверен, что вы, возможно, получите что-то подобное, работающее под PHP/Python.

Надеюсь, что это поможет.

2

Настройка тестовых данных для модульных испытаний может стать проблемой.

Когда речь идет о Java, если вы используете Spring API для модульного тестирования, вы можете контролировать транзакции на уровне единицы. Другими словами, вы можете выполнять модульные тесты, которые включают обновления баз/вставки/удаления и откаты изменений. В конце выполнения вы оставите все в базе данных, как это было до начала выполнения. Для меня это так хорошо, как может.

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