Подтвердить что ты не робот

SpecFlow и сложные объекты

Я оцениваю SpecFlow, и я немного застрял.
Все образцы, которые я нашел, в основном с простыми объектами.

Проект, над которым я работаю, сильно зависит от сложного объекта. Ближайшим образцом может быть этот объект:

public class MyObject
{
    public int Id { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public IList<ChildObject> Children { get; set; }

}

public class ChildObject
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Length { get; set; }
}

Кто-нибудь знает, как написать мои функции/сценарии, где MyObject будет создаваться с шага "данный" и использоваться в шагах "Когда" и "Далее"?

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

EDIT: Только один снимок: поддерживаются ли вложенные таблицы?

4b9b3361

Ответ 1

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

Ответ 2

Я бы сказал, что Маркус здесь очень прав, но я бы написал свой сценарий, чтобы использовать некоторые из методов расширения в пространстве имен TechTalk.SpecFlow.Assist. См. здесь.

Given I have the following Children:
| Id | Name | Length |
| 1  | John | 26     |
| 2  | Kate | 21     |
Given I have the following MyObject:
| Field     | Value      |
| Id        | 1          |
| StartDate | 01/01/2011 |
| EndDate   | 01/01/2011 |
| Children  | 1,2        |

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

    [Given(@"I have the following Children:")]
    public void GivenIHaveTheFollowingChildren(Table table)
    {
        ScenarioContext.Current.Set(table.CreateSet<ChildObject>());
    }


    [Given(@"I have entered the following MyObject:")]
    public void GivenIHaveEnteredTheFollowingMyObject(Table table)
    {
        var obj = table.CreateInstance<MyObject>();
        var children = ScenarioContext.Current.Get<IEnumerable<ChildObject>>();
        obj.Children = new List<ChildObject>();

        foreach (var row in table.Rows)
        {
            if(row["Field"].Equals("Children"))
            {
                foreach (var childId in row["Value"].Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries))
                {
                    obj.Children.Add(children
                        .Where(child => child.Id.Equals(Convert.ToInt32(childId)))
                        .First());
                }
            }
        }
    }

Надеюсь, что это (или часть этого) поможет вам

Ответ 3

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

С учетом сказанного вам все еще нужен способ выразить иерархические структуры в ваших спецификациях, то есть с помощью Gherkin. Насколько я знаю, это невозможно, и из этот пост (в группе SpecFlow Google) кажется, что это обсуждалось ранее.

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

Given I have the following hierarchical structure:
| MyObject.Id | StartDate | EndDate  | ChildObject.Id | Name | Length |
| 1           | 20010101  | 20010201 |                |      |        |
|             |           |          | 1              | Me   | 196    |
|             |           |          | 2              | You  | 120    |

Это не супер-довольно, я признаю, но это может сработать.

Другой способ сделать это - использовать значения по умолчанию и просто дать разницу. Вот так:

Given a standard My Object with the following children:
| Id | Name | Length |
| 1  | Me   | 196    |
| 2  | You  | 120    |

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

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

Надеюсь, что ответ на ваш вопрос...

Ответ 4

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

http://specflowcookbook.com/chapters/linking-table-rows/

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

Например:

Scenario: Letters to Santa appear in the emailers outbox

Given the following "Children" exist
| First Name | Last Name | Age |
| Noah       | Smith     | 6   |
| Oliver     | Thompson  | 3   |

And the following "Gifts" exist
| Child from Children    | Type     | Colour |
| Last Name is Smith     | Lego Set |        |
| Last Name is Thompson  | Robot    | Red    |
| Last Name is Thompson  | Bike     | Blue   |

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

Ответ 5

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

  • Сфокусируйтесь на бизнес-терминологии
  • Позволяет создавать легко читаемые сценарии
  • Обеспечьте уровень развязки между бизнес-терминологией и сложной моделью домена.

В качестве примера возьмем блог.

Сценарий SpecFlow: создание сообщения в блоге

Рассмотрим следующий сценарий, написанный так, чтобы кто-нибудь, знакомый с тем, как работает Блог, знает, что происходит:

Scenario: Creating a Blog Post
    Given a Blog named "Testing with SpecFlow" exists
    When I create a post in the "Testing with SpecFlow" Blog with the following attributes:
        | Field  | Value                       |
        | Title  | Complex Models              |
        | Body   | <p>This is not so hard.</p> |
        | Status | Working Draft               |
    Then a post in the "Testing with SpecFlow" Blog should exist with the following attributes:
        | Field  | Value                       |
        | Title  | Complex Models              |
        | Body   | <p>This is not so hard.</p> |
        | Status | Working Draft               |

Это моделирует сложные отношения, когда в блоге много сообщений в блоге.

Модель домена

Модель домена для этого приложения в блоге будет таковой:

public class Blog
{
    public string Name { get; set; }
    public string Description { get; set; }
    public IList<BlogPost> Posts { get; private set; }

    public Blog()
    {
        Posts = new List<BlogPost>();
    }
}

public class BlogPost
{
    public string Title { get; set; }
    public string Body { get; set; }
    public BlogPostStatus Status { get; set; }
    public DateTime? PublishDate { get; set; }

    public Blog Blog { get; private set; }

    public BlogPost(Blog blog)
    {
        Blog = blog;
    }
}

public enum BlogPostStatus
{
    WorkingDraft = 0,
    Published = 1,
    Unpublished = 2,
    Deleted = 3
}

Обратите внимание, что наш сценарий имеет "статус" со значением "Рабочий проект", но перечисление BlogPostStatus имеет WorkingDraft. Как вы переводите этот статус "естественного языка" на перечисление? Теперь введите тестовую модель.

Модель тестирования: BlogPostRow

Класс BlogPostRow предназначен для выполнения нескольких действий:

  • Переведите таблицу SpecFlow на объект
  • Обновите свою модель домена с заданными значениями
  • Предоставьте "конструктор копирования", чтобы вывести объект BlogPostRow со значениями из существующего экземпляра модели домена, чтобы вы могли сравнивать эти объекты в SpecFlow

код:

class BlogPostRow
{
    public string Title { get; set; }
    public string Body { get; set; }
    public DateTime? PublishDate { get; set; }
    public string Status { get; set; }

    public BlogPostRow()
    {
    }

    public BlogPostRow(BlogPost post)
    {
        Title = post.Title;
        Body = post.Body;
        PublishDate = post.PublishDate;
        Status = GetStatusText(post.Status);
    }

    public BlogPost CreateInstance(string blogName, IDbContext ctx)
    {
        Blog blog = ctx.Blogs.Where(b => b.Name == blogName).Single();
        BlogPost post = new BlogPost(blog)
        {
            Title = Title,
            Body = Body,
            PublishDate = PublishDate,
            Status = GetStatus(Status)
        };

        blog.Posts.Add(post);

        return post;
    }

    private BlogPostStatus GetStatus(string statusText)
    {
        BlogPostStatus status;

        foreach (string name in Enum.GetNames(typeof(BlogPostStatus)))
        {
            string enumName = name.Replace(" ", string.Empty);

            if (Enum.TryParse(enumName, out status))
                return status;
        }

        throw new ArgumentException("Unknown Blog Post Status Text: " + statusText);
    }

    private string GetStatusText(BlogPostStatus status)
    {
        switch (status)
        {
            case BlogPostStatus.WorkingDraft:
                return "Working Draft";
            default:
                return status.ToString();
        }
    }
}

Он находится в закрытых GetStatus и GetStatusText, где значения статуса сообщения для читаемого человека передаются в Enums и наоборот.

(Раскрытие: я знаю, что Enum не самый сложный случай, но это простой в использовании случай)

Последний фрагмент головоломки - это определения шага.

Использование тестовых моделей с вашей моделью домена в определениях шага

Шаг:

Given a Blog named "Testing with SpecFlow" exists

Определение:

[Given(@"a Blog named ""(.*)"" exists")]
public void GivenABlogNamedExists(string blogName)
{
    using (IDbContext ctx = new TestContext())
    {
        Blog blog = new Blog()
        {
            Name = blogName
        };

        ctx.Blogs.Add(blog);
        ctx.SaveChanges();
    }
}

Шаг:

When I create a post in the "Testing with SpecFlow" Blog with the following attributes:
    | Field  | Value                       |
    | Title  | Complex Models              |
    | Body   | <p>This is not so hard.</p> |
    | Status | Working Draft               |

Определение:

[When(@"I create a post in the ""(.*)"" Blog with the following attributes:")]
public void WhenICreateAPostInTheBlogWithTheFollowingAttributes(string blogName, Table table)
{
    using (IDbContext ctx = new TestContext())
    {
        BlogPostRow row = table.CreateInstance<BlogPostRow>();
        BlogPost post = row.CreateInstance(blogName, ctx);

        ctx.BlogPosts.Add(post);
        ctx.SaveChanges();
    }
}

Шаг:

Then a post in the "Testing with SpecFlow" Blog should exist with the following attributes:
    | Field  | Value                       |
    | Title  | Complex Models              |
    | Body   | <p>This is not so hard.</p> |
    | Status | Working Draft               |

Определение:

[Then(@"a post in the ""(.*)"" Blog should exist with the following attributes:")]
public void ThenAPostInTheBlogShouldExistWithTheFollowingAttributes(string blogName, Table table)
{
    using (IDbContext ctx = new TestContext())
    {
        Blog blog = ctx.Blogs.Where(b => b.Name == blogName).Single();

        foreach (BlogPost post in blog.Posts)
        {
            BlogPostRow actual = new BlogPostRow(post);

            table.CompareToInstance<BlogPostRow>(actual);
        }
    }
}

(TestContext - некоторая постоянная память данных, время жизни которой является текущим сценарием)

Модели в более широком контексте

Сделав шаг назад, термин "модель" стал более сложным, и мы только что представили еще одну модель. Посмотрите, как все они играют вместе:

  • Модель домена: класс, моделирующий то, что бизнес хочет часто хранить в базе данных, и содержит поведение, моделирующее бизнес-правила.
  • Модель просмотра: ориентированная на презентацию версия вашей модели домена.
  • Объект передачи данных: сумка данных, используемых для передачи данных с одного уровня или компонента на другой (часто используется с вызовами веб-службы).
  • Модель тестирования: объект, используемый для представления тестовых данных в усадьбе, который имеет смысл для делового человека, читающего ваши тесты поведения. Переводит между моделью домена и тестовой моделью.

Вы можете почти думать о тестовой модели как модели просмотра для ваших тестов SpecFlow, а "представление" - это сценарий, написанный в Gherkin.

Ответ 6

Хорошей идеей является повторное использование стандартного шаблона соглашения об именах MVC Model Binder в методе StepArgumentTransformation. Вот пример: Возможна ли привязка модели без mvc?

Вот часть кода (просто основная идея, без каких-либо подтверждений и ваших дополнительных требований):

В функциях:

Then model is valid:
| Id  | Children[0].Id | Children[0].Name | Children[0].Length | Children[1].Id | Children[1].Name | Children[1].Length |
| 1   | 222            | Name0            | 5                  | 223            | Name1            | 6                  |

В шагах:

[Then]
public void Then_Model_Is_Valid(MyObject myObject)
{
    // use your binded object here
}

[StepArgumentTransformation]
public MyObject MyObjectTransform(Table table)
{
    var modelState = new ModelStateDictionary();
    var model = new MyObject();
    var state = TryUpdateModel(model, table.Rows[0].ToDictionary(pair => pair.Key, pair => pair.Value), modelState);

    return model;
}

Это работает для меня.

Конечно, вы должны иметь ссылку на библиотеку System.Web.Mvc.