web-dev-qa-db-fra.com

SpecFlow et objets complexes

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?

20
Ramunas

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é. 

19
Lazydev

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

25
stuartf

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 ...

9
Marcus Hammarberg

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:

  • Être concentré sur la terminologie des affaires
  • Vous permettent de créer des scénarios faciles à lire
  • Fournir une couche de découplage entre la terminologie commerciale et le modèle de domaine complexe

Prenons un blog comme exemple.

Scénario SpecFlow: création d'un article de blog

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

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.

Le modèle de test: BlogPostRow

La classe BlogPostRow est censée faire quelques choses:

  1. Traduisez votre table SpecFlow en objet
  2. Mettez à jour votre modèle de domaine avec les valeurs données
  3. Fournissez un "constructeur de copie" pour associer un objet BlogPostRow aux valeurs d'une instance de modèle de domaine existante afin de pouvoir comparer ces objets dans SpecFlow

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.

Utilisation de modèles de test avec votre modèle de domaine dans les définitions d'étape

É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)

Modèles dans un contexte plus large

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:

  • Modèle de domaine: Une classe modélisant ce que l'entreprise souhaite souvent être stocké dans une base de données et contenant le comportement modélisant les règles de gestion.
  • Voir le modèle: Une version de votre modèle de domaine axée sur la présentation
  • Objet de transfert de données: ensemble de données utilisé pour transférer des données d'une couche ou d'un composant à un autre (souvent utilisé avec des appels de service Web)
  • Modèle de test: un objet utilisé pour représenter les données de test d'une manière qui a du sens pour un homme d'affaires lisant vos tests de comportement. Traduit entre le modèle de domaine et le modèle de test.

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.

5
Greg Burghardt

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.

3
user3202264

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.

1
Vetal

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>());
    }
0
Gomes