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: Только один снимок: поддерживаются ли вложенные таблицы?
Ответы
Ответ 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.
Ответ 7
с помощью TechTalk.SpecFlow.Assist;
https://github.com/techtalk/SpecFlow/wiki/SpecFlow-Assist-Helpers
[Given(@"resource is")]
public void Given_Resource_Is(Table payload)
{
AddToScenarioContext("payload", payload.CreateInstance<Part>());
}