Как unit test объект с запросами базы данных

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

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

Ответ 1

Я предлагаю высмеять ваши звонки в базу данных. 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();
    }
}

Теперь в вашем unit test вы создаете макет 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)
    }
}

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

Ответ 2

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

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

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

Уровень приложения также иногда называют "бизнес-слоем".

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

Другой вариант - использовать mocking/stubs.

Ответ 3

Самый простой способ unit test объекта с доступом к базе данных - использовать области транзакций.

Например:

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

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

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

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

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

Ответ 6

Параметры, которые у вас есть:

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

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

    класс MockDatabase расширяет базу данных { public Запрос результата (String query) {   return "mock result"; } }

    класс ObjectThatUsesDB {  public ObjectThatUsesDB (База данных db) {  this.database = db;  } }

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

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

Ответ 7

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

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

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

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

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

Ответ 11

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

Ответ 12

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

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

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

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

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

Ответ 13

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

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