c# - tutorial - toolsqa bdd
SpecFlow y objetos complejos (7)
Diría que Marcus es bastante correcto aquí, sin embargo, escribiría mi escenario para poder usar algunos de los métodos de extensiones en el espacio de nombres TechTalk.SpecFlow.Assist. Ver here
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 |
Para el código que se encuentra detrás de los pasos, podrías usar algo como esto, un poco más de manejo de errores.
[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());
}
}
}
}
Espero que esto (o algo de esto) te ayude.
Estoy evaluando SpecFlow y estoy un poco atascado.
Todas las muestras que he encontrado son básicamente con objetos simples.
El proyecto en el que estoy trabajando se basa en gran medida en un objeto complejo. Una muestra cercana podría ser este objeto:
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; }
}
¿Alguien tiene alguna idea de cómo podría escribir mis características / escenarios donde MyObject
se instanciara desde un paso "Dado" y se usara en los pasos "Cuándo" y "Luego"?
Gracias por adelantado
EDITAR: Sólo un disparo en mente: ¿son compatibles las tablas anidadas?
He trabajado en varias organizaciones ahora que se han encontrado con el mismo problema que describe aquí. Esta es una de las cosas que me impulsaron a (intentar) comenzar a escribir un libro sobre el tema.
http://specflowcookbook.com/chapters/linking-table-rows/
Aquí sugiero usar una convención que le permita usar los encabezados de la tabla de flujo de especificaciones para indicar de dónde provienen los elementos vinculados, cómo identificar cuáles desea, y luego usar el contenido de las filas para proporcionar los datos para "buscar" en el Mesas extranjeras.
Por ejemplo:
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 |
Esperemos que esto sea de alguna ayuda.
Le sugeriría que intente mantener sus escenarios lo más limpios posible, centrándose en la legibilidad para las personas sin experiencia en su proyecto. La forma en que se construyen los gráficos de objetos complejos se maneja en las definiciones de los pasos.
Dicho esto, aún necesita una forma de expresar estructuras jerárquicas en sus especificaciones, es decir, con Gherkin. Por lo que sé, no es posible y, a partir de esta publicación (en el grupo de Google SpecFlow), parece que se ha discutido anteriormente.
Básicamente, puedes inventar un formato propio y analizarlo en tu paso. No me he encontrado con esto, pero creo que probaría una tabla con valores en blanco para el siguiente nivel y analizaría eso en la definición del paso. Me gusta esto:
Given I have the following hierarchical structure:
| MyObject.Id | StartDate | EndDate | ChildObject.Id | Name | Length |
| 1 | 20010101 | 20010201 | | | |
| | | | 1 | Me | 196 |
| | | | 2 | You | 120 |
No es muy bonito, lo admito, pero podría funcionar.
Otra forma de hacerlo es usar los valores predeterminados y simplemente dar las diferencias. Me gusta esto:
Given a standard My Object with the following children:
| Id | Name | Length |
| 1 | Me | 196 |
| 2 | You | 120 |
En su definición de pasos, a continuación, agregue los valores "estándar" para MyObject y complete la lista de elementos secundarios. Ese enfoque es un poco más legible si me preguntas, pero tienes que "saber" qué es un MyObject estándar y cómo está configurado.
Básicamente - Gherkin no lo soporta. Pero puedes crear un formato que puedas analizar tú mismo.
Espero que esta responda a tu pregunta ...
Por el ejemplo que has mostrado, diría que lo estás haciendo mal . Este ejemplo parece más adecuado para escribir con nunit y, probablemente, con un objeto madre . Las pruebas escritas con flujo de especificaciones o una herramienta similar deben ser dirigidas al cliente y usar el mismo lenguaje que su cliente usaría para describir la característica.
Una buena idea es reutilizar el patrón de convención de nomenclatura estándar de MVC Model Binder en un método StepArgumentTransformation. Aquí hay un ejemplo: ¿Es posible la vinculación de modelos sin mvc?
Aquí está parte del código (solo la idea principal, sin validaciones y sus requisitos adicionales):
En características:
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 |
En pasos:
[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;
}
Esto funciona para mi.
Por supuesto, debe tener una referencia a la biblioteca System.Web.Mvc.
Voy un paso más allá cuando mi Modelo de objetos de dominio comienza a complicarse, y creo "Modelos de prueba" que utilizo específicamente en mis escenarios SpecFlow. Un modelo de prueba debe:
- Enfocarse en la terminología de negocios
- Le permite crear escenarios fáciles de leer
- Proporcionar una capa de desacoplamiento entre la terminología de negocios y el complejo modelo de dominio.
Tomemos un blog como ejemplo.
El escenario SpecFlow: crear una publicación de blog
Considere el siguiente escenario escrito para que cualquiera que esté familiarizado con el funcionamiento de un Blog sepa lo que está pasando:
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 |
Esto modela una relación compleja, donde un blog tiene muchas publicaciones de blog.
El modelo de dominio
El Modelo de Dominio para esta aplicación de Blog sería este:
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
}
Tenga en cuenta que nuestro escenario tiene un "Estado" con un valor de "Borrador de trabajo", pero la enumeración WorkingDraft
tiene WorkingDraft
. ¿Cómo se traduce ese estado de "lenguaje natural" a una enumeración? Ahora ingrese el modelo de prueba.
El modelo de prueba: BlogPostRow
La clase BlogPostRow
está destinada a hacer algunas cosas:
- Traduce tu tabla SpecFlow a un objeto
- Actualice su Modelo de Dominio con los valores dados
- Proporcione un "constructor de copia" para inicializar un objeto BlogPostRow con valores de una instancia de Modelo de dominio existente para que pueda comparar estos objetos en SpecFlow
Código:
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();
}
}
}
Es en el texto privado GetStatus
y GetStatusText
donde los valores de estado de las publicaciones de blog legibles por humanos se traducen a Enums, y viceversa.
(Divulgación: sé que un Enum no es el caso más complejo, pero es un caso fácil de seguir)
La última pieza del rompecabezas son las definiciones de los pasos.
Uso de modelos de prueba con su modelo de dominio en definiciones de pasos
Paso:
Given a Blog named "Testing with SpecFlow" exists
Definición:
[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();
}
}
Paso:
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 |
Definición:
[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();
}
}
Paso:
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 |
Definición:
[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
- Algún tipo de almacén de datos persistente cuya vida útil es el escenario actual)
Modelos en un contexto más amplio.
Dando un paso atrás, el término "Modelo" se ha vuelto más complejo, y acabamos de presentar otro tipo de modelo. Veamos cómo juegan todos juntos:
- Modelo de dominio: una clase que modela lo que la empresa quiere a menudo se almacena en una base de datos, y contiene el comportamiento que modela las reglas de negocios.
- Ver modelo: una versión centrada en la presentación de su modelo de dominio.
- Objeto de transferencia de datos: una bolsa de datos que se usa para transferir datos de una capa o componente a otro (a menudo se usa con llamadas de servicio web)
- Modelo de prueba: un objeto utilizado para representar datos de prueba de una manera que tiene sentido para una persona de negocios que lee sus pruebas de comportamiento. Se traduce entre el modelo de dominio y el modelo de prueba.
Casi se puede pensar en un modelo de prueba como un modelo de vista para sus pruebas de SpecFlow, con la "vista" siendo el escenario escrito en Gherkin.
utilizando TechTalk.SpecFlow.Assist;
[Given(@"resource is")]
public void Given_Resource_Is(Table payload)
{
AddToScenarioContext("payload", payload.CreateInstance<Part>());
}