Указание значений свойств [только для чтения] [через аргументы ctor] при создании экземпляров [неизменяемых] объектов с помощью AutoFixture

Мой тест требует, чтобы я установил свойство Response неизменяемого объекта Rsvp (см. ниже) на определенное значение.

public class Rsvp
{
    public string Response { get; private set; }

    public Rsvp(string response)
    {
        Response = response;
    }
}

Сначала я пытался сделать это с помощью Build<Rsvp>().With(x => x.Rsvp, "Attending"), но понял, что это поддерживает только записываемые свойства.

Я заменил это на Build<Rsvp>().FromFactory(new Rsvp("Attending")). Это работает, но обременительно для более сложных объектов, где не имеет значения, каковы некоторые свойства.

Например, если бы объект Rsvp имел свойство CreatedDate, этот метод создания экземпляра объекта заставил бы меня написать Build<Rsvp>().FromFactory(new Rsvp("Attending", fixture.Create<DateTime>())).

Есть ли способ указать значения только для смысловых свойств неизменяемого объекта?


person Matt Slavicek    schedule 27.12.2013    source источник
comment
Связано: stackoverflow.com/a/17851410/126014   -  person Mark Seemann    schedule 28.12.2013
comment
Build<T>().FromFactory(Func<T> factory) выдает InvalidCastException, когда я пытаюсь это сделать. Это все еще работает в AutoFixture 4.0?   -  person John Zabroski    schedule 13.12.2020


Ответы (3)


Изначально AutoFixture создавался как инструмент для разработки через тестирование (TDD), а TDD — это обратная связь. В духе GOOS вам следует прислушиваться к своим тестам. Если тесты сложно писать, вам следует подумать о дизайне вашего API. AutoFixture имеет тенденцию усиливать такого рода отзывы.

Откровенно говоря, неизменяемые типы — это боль в C#, но вы можете упростить работу с таким классом, как Rsvp, если возьмете пример из F# и введете семантику copy and update. Если вы измените Rsvp таким образом, с ним будет намного проще работать в целом и, следовательно, как побочный продукт, также и для модульного тестирования:

public class Rsvp
{
    public string Response { get; private set; }

    public DateTime CreatedDate { get; private set; }

    public Rsvp(string response, DateTime createdDate)
    {
        Response = response;
        CreatedDate = createdDate;
    }

    public Rsvp WithResponse(string newResponse)
    {
        return new Rsvp(newResponse, this.CreatedDate);
    }

    public Rsvp WithCreatedDate(DateTime newCreatedDate)
    {
        return new Rsvp(this.Response, newCreatedDate);
    }
}

Обратите внимание, что я добавил два метода WithXyz, которые возвращают новый экземпляр с одним измененным значением, но все остальные значения остаются постоянными.

Это позволит вам создать экземпляр Rsvp для тестирования следующим образом:

var fixture = new Fixture();
var seed = fixture.Create<Rsvp>();
var sut = seed.WithResponse("Attending");

или, как однострочный:

var sut = new Fixture().Create<Rsvp>().WithResponse("Attending");

Если вы не можете изменить Rsvp, вы можете добавить методы WithXyz в качестве методов расширения.

Как только вы сделаете это около дюжины раз, вам это надоест, и пришло время перейти на F #, где все это (и многое другое) встроено:

type Rsvp = {
    Response : string
    CreatedDate : DateTime }

Вы можете создать запись Rsvp с AutoFixture следующим образом:

let fixture = Fixture()
let seed = fixture.Create<Rsvp>()
let sut = { seed with Response = "Attending" }

или, как однострочный:

let sut = { Fixture().Create<Rsvp>() with Response = "Attending" }
person Mark Seemann    schedule 28.12.2013
comment
+1 Если @mattslav может изменить класс Rsvp (или добавить методы расширения), то это должен быть принятый ответ. - person Nikos Baxevanis; 29.12.2013
comment
Первый абзац этого ответа написан настолько хорошо, что мы могли бы включить его в описание в файле README AutoFixture. - person Nikos Baxevanis; 29.12.2013
comment
С записями в F# вы теряете возможность определять инварианты свойств. Возможно, вы захотите ограничить форму строкового свойства подмножеством всех строк. Вот где классы светятся. - person fsl; 30.12.2013
comment
Я бы не сказал, что тесты сложно писать. Я не был уверен, что лучше всего реализовать шаблон Object Builder, используя AutoFixture в качестве механизма для создания объектов. Использование методов расширения, которые доступны только для тестов, кажется лучшим вариантом, поскольку это то, что должны уметь делать только тесты. - person Matt Slavicek; 30.12.2013
comment
@mattslav Конечно, вы можете добавить его как код только для тестирования, но IME, это ценная функция, которую можно использовать и в рабочем коде. Преимущество TDD заключается в том, что он дает вам обратную связь о производственном API вашего кода. - person Mark Seemann; 30.12.2013
comment
@MarkSeemann В вашей реализации Rsvp похоже, что у вас есть те же возможности (включая цепочку), как если бы вы реализовали вложенный класс RsvpBuilder с WithXyz() методами. Значит ли это, что вложенный билдер не делает ничего, кроме инкапсуляции логики создания и (если конструкторы Rsvp сделаны закрытыми) гарантирует его статус «единственной точки входа» в конструкцию Rsvp? - person Jeff; 19.01.2014
comment
@MarkSeemann Кроме того, сохраняя открытый конструктор Rsvp при сохранении неизменности, это то, что позволяет Autofixture по-прежнему создавать эти неизменяемые объекты для нас, не жалуясь на то, что «... нет общедоступного конструктора, это абстрактный или закрытый тип ... '? - person Jeff; 19.01.2014
comment
@Lumirris С API copy-and-update класс становится собственным Builder, так что вам не нужен отдельный Builder. Мне больше нравится этот шаблон, так как он более краткий. FWIW, я украл идею у F#. Сохранение конструктора (и самого класса) общедоступным означает, что это просто «обычный» конкретный класс, и AutoFixture работает с ним без дополнительных расширений или настроек. Это пока-йоке дизайн: blog.ploeh.dk/2011/05/24. /Poka-yokeDesignFromSmelltoFragrance - person Mark Seemann; 19.01.2014
comment
@MarkSeemann В сценарии, в котором создание экземпляра объекта обходится дорого, не становится ли этот подход менее желательным по мере того, как делается больше вызовов WithAbc, ... WithXyz (и новые экземпляры создаются при каждом вызове?). Или это просто баланс, который необходимо соблюдать, когда нужны неизменяемые классы? - person Jeff; 19.01.2014
comment
@MarkSeemann Я знаком с вашей серией блогов Poka-yoke. ; какая из 5 частей серии конкретно посвящена этому вопросу (если есть) - я не мог понять. - person Jeff; 19.01.2014
comment
@Lumirris Что это за сценарий, когда создание объекта обходится дорого? - person Mark Seemann; 19.01.2014
comment
@MarkSeemann Хе-хе. Полагаю, вы раскрыли мой блеф, так как я не могу привести ни одного конкретного примера, но... разве нет сценариев, в которых это было бы дорого? Я предполагаю, что что-то просто чувствовалось неправильным, когда я видел новые экземпляры, созданные для каждого связанного вызова WithXyz, когда было сделано много вызовов... - person Jeff; 19.01.2014
comment
@Lumirris Если вы не сделаете все возможное, чтобы сделать создание экземпляров дорогим, это не так. blog.ploeh.dk/2011/03/04/Создавайте графы объектов с уверенностью - person Mark Seemann; 20.01.2014
comment
Хотя я согласен с тем, что лучше использовать F#, к сожалению, моя группа пока не хочет иметь производственный код F#, поэтому я застрял на C#. Однако я добился поддержки неизменяемых типов. При тестировании с ними может быть очень удобно иметь анонимную переменную с определенным свойством, установленным на определенное значение. Например, при использовании объекта параметров мне может понадобиться написать тест для случая x.Foo == True, а другой — для случая x.Foo == False, но это проблематично, если для x имеется много свойств. В этом случае нам не нужен производственный код копирования и обновления для x, только для тестирования. - person Matt Klein; 02.09.2016

Пока свойство Response доступно только для чтения*, вы можете определить собственное SpecimenBuilder для типа Rsvp:

internal class RsvpBuilder : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as ParameterInfo;
        if (pi == null)
            return new NoSpecimen();

        if (pi.ParameterType != typeof(string) || pi.Name != "response")
            return new NoSpecimen();

        return "Attending";
    }
}

Проходит следующий тест:

[Fact]
public void ResponseIsCorrect()
{
    var fixture = new Fixture();
    fixture.Customizations.Add(new RsvpBuilder());
    var sut = fixture.Create<Rsvp>();

    var actual = sut.Response;

    Assert.Equal("Attending", actual);
}

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

person Nikos Baxevanis    schedule 28.12.2013
comment
Я надеялся на решение, которое не требовало создания построителя объектов для каждого объекта. Кроме того, тесту нужно будет передать значение Response в RsvpBuilder, как это сделать? - person Matt Slavicek; 28.12.2013
comment
@mattslav Возможно, как ответил Mark Seemann. - person Nikos Baxevanis; 29.12.2013

Расширяя ответ Никоса, мы можем обобщить настройку для работы с любым свойством как таковым:

public class OverridePropertyBuilder<T, TProp> : ISpecimenBuilder
{
    private readonly PropertyInfo _propertyInfo;
    private readonly TProp _value;

    public OverridePropertyBuilder(Expression<Func<T, TProp>> expr, TProp value)
    {
        _propertyInfo = (expr.Body as MemberExpression)?.Member as PropertyInfo ??
                        throw new InvalidOperationException("invalid property expression");
        _value = value;
    }

    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as ParameterInfo;
        if (pi == null)
            return new NoSpecimen();

        var camelCase = Regex.Replace(_propertyInfo.Name, @"(\w)(.*)",
            m => m.Groups[1].Value.ToLower() + m.Groups[2]);

        if (pi.ParameterType != typeof(TProp) || pi.Name != camelCase)
            return new NoSpecimen();

        return _value;
    }
}

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

public class FixtureCustomization<T>
{
    public Fixture Fixture { get; }

    public FixtureCustomization(Fixture fixture)
    {
        Fixture = fixture;
    }

    public FixtureCustomization<T> With<TProp>(Expression<Func<T, TProp>> expr, TProp value)
    {
        Fixture.Customizations.Add(new OverridePropertyBuilder<T, TProp>(expr, value));
        return this;
    }

    public T Create() => Fixture.Create<T>();
}

public static class CompositionExt
{
    public static FixtureCustomization<T> For<T>(this Fixture fixture)
        => new FixtureCustomization<T>(fixture);
}

затем мы используем его в вашем примере как:

var obj = 
  new Fixture()
    .For<Rsvp>()
    .With(x => x.Response, "Attending")
    .Create();
person Fabio Marreco    schedule 25.07.2019