web-dev-qa-db-ja.com

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がインスタンス化され、「いつ」および「その後」のステップで使用される私の機能/シナリオをどのように書くことができるか考えていますか?

前もって感謝します

編集:念のため:ネストされたテーブルはサポートされていますか?

21
Ramunas

あなたが示した例では、私は あなたはそれを間違っている と言うでしょう。この例は、nunitで記述し、おそらくobject motherを使用して書くのに適しています。 specflowまたは類似のツールで記述されたテストは、顧客向けであり、顧客が機能を説明するために使用するのと同じ言語を使用する必要があります。

20
Lazydev

ここではマーカスはほぼ正しいと思いますが、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());
                }
            }
        }
    }

これ(またはこの一部)があなたに役立つことを願っています

29
stuartf

プロジェクトの技術者以外の人にとっては読みやすさに重点を置き、シナリオをできるだけクリーンに保つことをお勧めします。複雑なオブジェクトグラフがどのように構築されるかは、ステップ定義で処理されます。

それでも、仕様で階層構造を表現する方法が必要です(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とは何か、およびその構成方法を「知っている」必要があります。

基本的に、ガーキンはそれをサポートしていません。ただし、自分で解析できる形式を作成できます。

これがあなたの質問に答えることを願っています...

10

ドメインオブジェクトモデルが複雑になり始めたら、さらに一歩進んで、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 enumにはWorkingDraftがあることに注意してください。その「自然言語」ステータスを列挙型にどのように変換しますか?次に、テストモデルを入力します。

テストモデル:BlogPostRow

BlogPostRowクラスは、いくつかのことを行うためのものです。

  1. SpecFlowテーブルをオブジェクトに変換する
  2. ドメインモデルを指定された値で更新します
  3. SpecPostFlowでこれらのオブジェクトを比較できるように、既存のドメインモデルインスタンスからの値でBlogPostRowオブジェクトをシードする「コピーコンストラクター」を提供します。

コード:

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

それはプライベートGetStatusGetStatusTextにあり、人間が読めるブログ投稿のステータス値がEnumに変換され、その逆も同様です。

(開示: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-存続期間が現在のシナリオである、ある種の永続データストア)

より大きなコンテキストのモデル

一歩下がると、「モデル」という用語はより複雑になり、もう1つの種類のモデルを導入しました。それらがどのように一緒に遊ぶか見てみましょう:

  • ドメインモデル:ビジネスが望むものをデータベースに格納することをモデル化するクラスであり、ビジネスルールをモデル化する動作を含みます。
  • モデルの表示:ドメインモデルのプレゼンテーション中心のバージョン
  • データ転送オブジェクト:あるレイヤーまたはコンポーネントから別のレイヤーまたはコンポーネントにデータを転送するために使用されるデータのバッグ(多くの場合、Webサービス呼び出しで使用されます)
  • テストモデル:行動テストを読んでいるビジネスマンにとって意味のある方法でテストデータを表すために使用されるオブジェクト。ドメインモデルとテストモデルの間で変換します。

ほとんどの場合、テストモデルはSpecFlowテストのビューモデルと考えることができます。「ビュー」はGherkinで記述されたシナリオです。

6
Greg Burghardt

私はいくつかの組織で働いており、ここで説明する同じ問題にすべて遭遇しました。これは、私がその主題について本を書き始めることを(試みた)ように促したものの1つです。

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

ここでは、specflowテーブルのヘッダーを使用して、リンクされたアイテムのソースを示し、必要なアイテムを識別する方法を示し、行のコンテンツを使用してデータを「ルックアップ」して外部テーブル。

例えば:

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   |

うまくいけば、これはいくつかの助けになるでしょう。

3
user3202264

StepArgumentTransformationメソッドで標準のMVCモデルバインダーの命名規則パターンを再利用することをお勧めします。次に例を示します。 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ライブラリへの参照が必要です。

1
Vetal

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