J'évalue SpecFlow et je suis un peu coincé.
Tous les échantillons que j'ai trouvés sont essentiellement des objets simples.
Le projet sur lequel je travaille s'appuie fortement sur un objet complexe. Un échantillon proche pourrait être cet objet:
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; }
}
Quelqu'un a-t-il une idée de la façon dont une personne pourrait écrire mes caractéristiques/scénarios dans lesquels MyObject
serait instancié à partir d'une étape "Donnée" et utilisé dans les étapes "Quand" et "Alors"?
Merci d'avance
EDIT: Juste un coup en tête: les tableaux imbriqués sont-ils supportés?
Pour l'exemple que vous avez montré, je dirais vous vous trompez . Cet exemple semble plus approprié pour écrire avec nunit et utiliser probablement un objetmère. Les tests écrits avec specflow ou un outil similaire doivent être en relation directe avec le client et utiliser le même langage que celui utilisé par votre client pour décrire la fonctionnalité.
Je dirais que Marcus a à peu près raison, mais je voudrais écrire mon scénario pour pouvoir utiliser certaines des méthodes d’extensions dans l’espace de noms TechTalk.SpecFlow.Assist. Voir ici .
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 |
Pour le code derrière les étapes, vous pouvez utiliser quelque chose comme ceci, ce qui entraînera un peu plus de gestion des erreurs.
[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());
}
}
}
}
J'espère que ceci (ou une partie de cela) vous aidera
Je suggérerais que vous essayiez de garder vos scénarios aussi propres que possible, en vous concentrant sur la lisibilité pour les personnes non-technophiles de votre projet. La manière dont les graphes d'objets complexes sont construits est ensuite traitée dans vos définitions d'étape.
Cela dit, vous avez toujours besoin d’un moyen d’exprimer les structures hiérarchiques dans vos spécifications, c’est-à-dire avec Gherkin. Autant que je sache, cela n’est pas possible et de cet article (dans le groupe Google de SpecFlow), il semble que cela ait déjà été discuté.
Fondamentalement, vous pouvez inventer votre propre format et l’analyser. Je ne me suis pas heurté à cela moi-même, mais je pense que j'essaierais une table avec des valeurs vides pour le niveau suivant et l'analyse dans la définition de l'étape. Comme ça:
Given I have the following hierarchical structure:
| MyObject.Id | StartDate | EndDate | ChildObject.Id | Name | Length |
| 1 | 20010101 | 20010201 | | | |
| | | | 1 | Me | 196 |
| | | | 2 | You | 120 |
Ce n'est pas très joli, je l'avoue, mais ça pourrait marcher.
Une autre façon de le faire est d'utiliser les valeurs par défaut et de simplement donner les différences. Comme ça:
Given a standard My Object with the following children:
| Id | Name | Length |
| 1 | Me | 196 |
| 2 | You | 120 |
Dans votre définition d’étape, vous ajoutez ensuite les valeurs "standard" pour MyObject et remplissez la liste des enfants. Cette approche est un peu plus lisible si vous me le demandez, mais vous devez "savoir" ce qu'est un MyObject standard et comment il est configuré.
Fondamentalement - Gherkin ne le supporte pas. Mais vous pouvez créer un format que vous pouvez analyser vous-même.
J'espère que cela répond à votre question ...
Je vais encore plus loin lorsque mon modèle d'objet de domaine commence à devenir complexe et crée des "modèles de test" que j'utilise spécifiquement dans mes scénarios SpecFlow. Un modèle de test devrait:
Prenons un blog comme exemple.
Considérez le scénario suivant pour que toute personne familiarisée avec le fonctionnement d'un blog sache ce qui se passe:
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 |
Ceci modélise une relation complexe, où un blog contient de nombreux articles de blog.
Le modèle de domaine pour cette application de blog serait le suivant:
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
}
Notez que notre scénario a un "statut" avec une valeur de "brouillon de travail", mais le BlogPostStatus
enum a WorkingDraft
. Comment traduisez-vous ce statut de "langage naturel" en une énumération? Maintenant, entrez le modèle de test.
La classe BlogPostRow
est censée faire quelques choses:
Code:
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();
}
}
}
C'est dans les variables privées GetStatus
et GetStatusText
que les valeurs de statut de publication de blog lisibles par l'homme sont traduites en Enums, et inversement.
(Divulgation: Je sais qu'un énum n'est pas le cas le plus complexe, mais c'est un cas facile à suivre)
La dernière pièce du puzzle est la définition des étapes.
Étape:
Given a Blog named "Testing with SpecFlow" exists
Définition:
[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();
}
}
Étape:
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 |
Définition:
[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();
}
}
Étape:
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 |
Définition:
[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
- Une sorte de magasin de données persistant dont la durée de vie correspond au scénario actuel)
En prenant du recul, le terme "modèle" est devenu plus complexe et nous venons tout juste d'introduire un type de modèle {encore. Voyons comment ils jouent tous ensemble:
Vous pouvez presque penser à un modèle de test en tant que modèle de vue pour vos tests SpecFlow, la "vue" étant le scénario écrit en Gherkin.
J'ai travaillé dans plusieurs organisations qui ont toutes rencontré le même problème que vous décrivez ici. C'est l'une des choses qui m'a poussé à (tenter) de commencer à écrire un livre sur le sujet.
http://specflowcookbook.com/chapters/linking-table-rows/
Ici, je suggère d’utiliser une convention qui vous permet d’utiliser les en-têtes de la table de spécflow pour indiquer d’où proviennent les éléments liés, comment identifier ceux que vous voulez, puis utiliser le contenu des lignes pour fournir les données à "rechercher" dans le fichier. tables étrangères.
Par exemple:
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 |
J'espère que cela vous sera utile.
Une bonne idée est de réutiliser le modèle de convention de dénomination standard de MVC Model Binder dans une méthode StepArgumentTransformation. Voici un exemple: La liaison de modèle est-elle possible sans MVC?
Voici une partie du code (juste l'idée principale, sans aucune validation et vos exigences supplémentaires):
Dans les fonctionnalités:
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 |
Par étapes:
[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;
}
Ça marche pour moi.
Bien entendu, vous devez avoir une référence à la bibliothèque System.Web.Mvc.
using 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>());
}