MVC, DI и Mock Objects - PullRequest
       14

MVC, DI и Mock Objects

2 голосов
/ 03 февраля 2010

Я пытаюсь обдумать реализацию MVC с максимально возможной полезной абстракцией для автоматизированного модульного тестирования. В процессе я столкнулся с интересной загадкой: как и где мне объявить фиктивный объект для моей базы данных?

Вот что у меня есть.

  • ContactView - это форма, которая реализует IContactView . Он обладает знанием IContactModel , поэтому может обновляться в ответ на уведомления о событиях от объекта модели.
  • Контакт - это класс, который реализует IContactModel . Он инкапсулирует бизнес-правила для управления объектом, а также код для извлечения / обновления данных на уровне доступа к данным.
  • ContactController - это класс, который реализует IContactController и обладает знаниями как IContactModel , так и IContactView .
  • База данных - это класс, который реализует IDatabase и содержит методы для выбора, вставки, обновления и удаления данных в базе данных.

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

Я бы предпочел, чтобы представления и контроллеры не знали, как хранятся данные. Таким образом, если бы я решил сделать это позже, я мог бы поменять IDatabase на что-то вроде IJsonStore или IXmlStore, и мне нужно было только прикоснуться к модельным классам. Я не хочу, чтобы мои представления и контролеры делали какие-либо предположения о том, где и как хранятся данные.

Я вижу пару возможных решений, но я не уверен, что из них самое лучшее.

  • Я могу объявить синглтон, который предоставляет публичное свойство, База данных (типа IDatabase ). Таким образом, модульные тесты могут установить для базы данных фиктивный объект, а производственный код - для производственной базы данных. Но это означает, что производственный код в какой-то момент должен был бы знать о IDatabase. Я полагаю, это неизбежно; но я надеюсь, что есть другое решение, которое предпочтительнее.
  • Я могу изменить классы модели, чтобы сохранить ссылку на базу данных, взяв ее в конструкторе. Это кажется нежелательным просто потому, что это приводит к большому количеству дополнительного кода. И я вернулся к исходной точке: кто бы ни объявил, что экземпляр модели должен знать, какой IDatabase объект, который я хочу использовать.

Я уверен, что есть альтернативы, и я просто не знаю о них. Итак, я добавлю это: как бы вы, ребята, сделали это, и что вы видели, что это сработало?

Заранее спасибо.

Ответы [ 3 ]

1 голос
/ 04 февраля 2010

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

Во-вторых, я бы не назвал эту вещь IDatabase. Слово База данных описывает реализацию, а не роль. В зависимости от вашего домена я мог бы назвать его AddressBook или Rolodex, или что-то, что говорит мне о контексте приложения. Это сохраняет код модели без каких-либо технических деталей реализации. Если бы я использовал базу данных для ее реализации, я мог бы затем вызвать класс DatabaseAddressBook. Я обычно нахожу, что придирчивость к такого рода разделению производит более ясный код и (иногда) более ясное мышление.

1 голос
/ 04 февраля 2010

Часто лучше сохранять объекты модели как POCO / POJO, и чтобы контроллеры заполняли модель (и представление), используя введенные зависимости.

По многим причинам Конструктор Инъекции - ваш лучший выбор по умолчанию для DI. Вот пример контроллера на основе C # с внедренной базой данных:

public class ContactController : IContactController
{
    private readonly IDatabase db;

    public ContactController(IDatabase db)
    {
        if (db == null)
        {
            throw new ArgumentNullException("db");
        }

        this.db = db;
    }

    public IContactView CreateView(int id)
    {
        var model = this.db.Select(id);
        return new ContactView(model);
    }
}

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

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

0 голосов
/ 12 октября 2010

Я решаю эту проблему самым простым способом: сделав Database класс статическим фасадом (который не требует реализации отдельного интерфейса Java).

ЭтоРешение не только делает производственный код настолько простым, насколько это возможно, но также и модульные тесты!Например, вот почти полный тест JUnit, использующий API-интерфейс JMockit Expectations (инструмент, который я создал для включения таких тестов):

public final class Database
{
    private Database() {}

    public static void save(Object transientEntity) { ...uses JPA/JDO... }
    ... other static methods ...
}

public class Contact
{
    public Contact(String s, int i) { ... }

    public void doSomeBusinessOperation()
    {
        ...
        Database.save(<some transient entity>);
        ...
    }
}

public final class ContactTest
{
    @Test
    public void doSomeBusinessOperationShouldSaveEntityToDatabase()
    {
        new Expectations()
        {
            Database db; // a mock field, causing "Database" to be mocked

            {
                // Records an expectation for the method to be called once:
                Database.save(any);
            }
        };

        // Exercises SUT, which should replay the expectation as recorded:
        new Contact("abc", 123).doSomeBusinessOperation();
    }
}
...