Как эффективно создавать и использовать паттерн-строитель

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

В настоящее время у меня есть шаблон T4, который создает, например, следующий конструктор для ученика:

public class StudentBuilder : Builder<Student, StudentBuilder>
{
    public StudentBuilder()
    {
        IsMale = true;
    }

    public StudentBuilder WithFirstName(string firstName)
    {
        this.FirstName = firstName;
        return this;
    }

    public StudentBuilder WithLastName(string lastName)
    {
        this.LastName = lastName;
        return this;
    }

    public StudentBuilder WithIsMale(bool isMale)
    {
        this.IsMale = isMale;
        return this;
    }

    internal override Student Construct()
    {
        Student result = new Student()
        {
            FirstName = FirstName ?? "FirstName:" + id.ToString(),
            LastName = LastName ?? "LastName:" + id.ToString(),
            IsMale = IsMale,
            Id = id,
        };

     /   return result;
    }
}

Через базовые классы я могу использовать это следующим образом:

Student wouter = StudentBuilder.Build()
    .WithFirstName("Wouter")
    .WithLastName("de Kort");
List<Student> students = StudentBuilder.Build().Multiple(10, (builder, index) => builder.WithFirstName("FirstName" + index));

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

Например, у ученика должен быть наставник, наставник принадлежит школе, школа - городу, город - ....

В результате получится такой код:

StudentBuilder.Build().WithMentor(MentorBuilder.Build().WithSchool(SchoolBuilder.Build().WithCity(CityBuilder.Build()))

Как мне это оптимизировать? Я думал о том, чтобы сделать «здание по умолчанию» в методе Construct каждого Строителя, но если бы я построил 10 учеников, это привело бы к 10 наставникам в 10 школах в 10 городах из 10 ....

Или, может быть, создание таких методов, как WithAllCity (..), WithAll (School)

Любые идеи? Правильно ли я использую шаблон Builder? Может ли помочь класс директора? Или мне следовало унаследовать классы от StudentBuilder, которые решают эти разные случаи?

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


person Wouter de Kort♦    schedule 09.11.2011    source источник
comment
We run our unit tests in memory on the development machines and against the database on the build server. - Это звучит болезненно для устранения неполадок. Как это работает для вашего проекта?   -  person Ritch Melton    schedule 09.11.2011
comment
Я использую visualstudiogallery.msdn.microsoft.com/ для создания различных настроек app.config с разделом единства для переключения из памяти в базу данных. Разработчики могут позволить модульным тестам нацеливаться на базу данных, переключив там конфигурацию сборки. Это ускоряет разработку (тесты памяти выполняются очень быстро), но также устраняет ошибки, которые не удается отловить подделкой памяти. Но тут сразу возникает проблема, почему я задаю этот вопрос. Очень длинная настройка компоновщика, чтобы убедиться, что тесты проходят через сервер сборки.   -  person Wouter de Kort♦    schedule 09.11.2011


Ответы (2)


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

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

person Joe Daley    schedule 09.11.2011
comment
Проблема в том, что мы запускаем наши модульные тесты также для реальной базы данных (интеграционные тесты на сервере сборки), а модель данных требует, чтобы эти свойства были установлены. Это также связано с тем, что иногда, например, люди забывают инициализировать DateTime, что вызывает ошибку в SQL Server, но не в памяти. - person Wouter de Kort♦; 09.11.2011
comment
Разделите модульный и интеграционный тесты. Сохраняйте индивидуальные модульные тесты (в памяти). Сохраняйте общие интеграционные тесты (с базой данных) и используйте полный набор данных, который используется несколькими тестами (что, как я знаю, именно то, чего вы пытаетесь избежать). На странице stackoverflow.com/questions/482827 есть несколько хороших предложений по упрощению использования общих тестовых данных, но я не думаю, что вы Следует постараться вообще избежать этого в интеграционных тестах. Извините, если это не ответ :) - person Joe Daley; 09.11.2011
comment
Я тестирую Presenters, какие службы имеют доступ к репозиториям. В интеграционных тестах используются WCF и Entity Framework, поэтому выполнение этих тестов занимает много времени. Но они обнаруживают ошибки, которые наши модульные тесты не обнаруживают, особенно в самой базе данных. Поэтому мне нужно провести модульное тестирование и тестирование интеграции моих докладчиков, и тестовый код точно такой же (который мы настроили сейчас с некоторой инъекцией зависимостей и поддельным ObjectContext в памяти). Я не могу создать отдельный интеграционный тест, потому что это будет дубликатом моих модульных тестов, но намного медленнее (что убило использование модульного теста в нашем последнем проекте) - person Wouter de Kort♦; 09.11.2011
comment
@Wouter - Я не вижу упоминания об использовании Mocks в ваших объяснениях. Вы почему-то избегаете их? Я думаю, что совет Джо очень точен. - person Ritch Melton; 09.11.2011
comment
@RitchMelton Я мог бы добавить это, но я думаю, что моя история становится немного длинной (или нет ??). Инфраструктура для модульных тестов работает без WCF и против фальшивого ObjectContext в памяти. На сервере сборки модульные тесты запускаются для реальной базы данных и через WCF, чтобы отловить любые ошибки интеграции, пропущенные модульными тестами. - person Wouter de Kort♦; 09.11.2011

Если вы собираетесь составлять списки учеников, вы можете составить list конструктор класса - StudentsBuilder. По умолчанию класс построителя сгенерирует список учеников с псевдослучайными свойствами, определенными вами. Это похоже на подход AutoPoco.

Я считаю, что создание собственного построителя списков является более гибким с точки зрения определения поведения при создании и поддержки любого типа класса. Я создаю класс-конструктор с IList<T> полями (аналогично подходу, основанному на структуре массивов, ориентированном на данные (SoA)).

public class StudentsBuilder
{
    private int _size;
    private IList<string> _firstNames; 
    private IList<string> _lastNames;
    private IList<MentorBuilder> _mentors;

    public StudentsBuilder(int size = 10)
    {
        _size = 10;
        _firstNames = new RandomStringGenerator(size).Generate();
        _lastNames = new RandomStringGenerator(size).Generate();
        _mentors = Enumerable.Range(0, size).Select(_ => new MentorBuilder()).ToList();
    }

    public StudentsBuilder WithFirstNames(params string[] firstNames)
    {
        _firstNames = firstNames;
        return this;
    }

    public IList<Student> Build()
    {
        students = new List<Student>();
        for (int i = 0; i < size; i++)
            students.Add(new Student(_firstNames[i], _lastNames[i], _mentors[i].Build());
        return students;
    }
}

Каждый список полей переопределяется с использованием отдельного метода, принимающего аргумент массива params. Вы также можете сделать списки полей общедоступными, чтобы использовать более интересный синтаксис With(Action<StudentsBuilder> action) для замены значений. Тестовый код выглядит так:

var students = new StudentBuilder(size: 4)
    .WithFirstNames("Jim", "John", "Jerry", "Judy")
    .Build();
person rybo103    schedule 22.08.2015