Как выполнить модульное тестирование объекта с запросами к базе данных - PullRequest
139 голосов
/ 27 августа 2008

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

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

Ответы [ 13 ]

75 голосов
/ 27 августа 2008

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

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

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

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

25 голосов
/ 27 августа 2008

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

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

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

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

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

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

11 голосов
/ 27 августа 2008

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

Например:

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

9 голосов
/ 07 сентября 2008

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

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

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

GetDatabase (). ExecuteSQL («INSERT INTO foo (бла, бла)»)

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

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

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

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

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

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

9 голосов
/ 27 августа 2008

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

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

5 голосов
/ 27 августа 2008

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

4 голосов
/ 27 августа 2008

Опции у вас есть:

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

    class Database {
     public Result query(String query) {... real db here ...}
    }</p>
    
    <p>class MockDatabase extends Database {
      public Result query(String query) { 
        return "mock result"; 
      }
    }</p>
    
    <p>class ObjectThatUsesDB {
     public ObjectThatUsesDB(Database db) { 
       this.database = db; 
     }
    }
    
    сейчас в работе вы используете обычную базу данных, и для всех тестов вы просто внедряете фиктивную базу данных, которую можете создать ad hoc.
  • Не используйте БД вообще в большей части кода (в любом случае, это плохая практика). Создайте объект «база данных», который вместо возврата с результатами вернет нормальные объекты (т.е. вернет User вместо кортежа {name: "marcin", password: "blah"}), напишите все ваши тесты с использованием специально созданных real объектов и напишите один большой тест, который зависит от базы данных, чтобы убедиться, что это преобразование работает нормально.

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

2 голосов
/ 19 ноября 2011

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

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

2 голосов
/ 27 августа 2008

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

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

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

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

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

2 голосов
/ 27 августа 2008

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

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