Модульное тестирование объекта с помощью запросов к базе данных


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

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

13 133

13 ответов:

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

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

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)
    }
}

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

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

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

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

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

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

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

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

например:

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

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

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

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

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

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

GetDatabase().ExecuteSQL ("вставить в foo (бла, бла)")

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

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

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

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

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

надеюсь, что это помогает.

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

варианты:

  • напишите сценарий, который уничтожит базу данных перед запуском модульных тестов, затем заполните БД предопределенным набором данных и запустите тесты. Вы также можете сделать это перед каждым тестом – это будет медленно, но менее подвержено ошибкам.
  • ввести базу данных. (Пример в псевдо-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 вместо кортежа!--2-->) напишите все свои тесты с помощью специальной конструкции реальные объекты и написать один большой тест, который зависит от базы данных, которая гарантирует, что это преобразование работает нормально.

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

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

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

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

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

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

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

вы могли бы использовать насмешливый рамки для абстрагирования ядра СУБД. Я не знаю, есть ли PHP/Python, но для типизированных языков (C#, Java и т. д.) есть много вариантов

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

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

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

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

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

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

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

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