5

VoteUp(int id) と VoteDown(int id) という 2 つの非常によく似たコントローラー アクションに対して、2 つの非常によく似た仕様があります。これらのメソッドにより、ユーザーは投稿に賛成票または反対票を投じることができます。StackOverflow の質問に対する賛成/反対の投票機能のようなものです。仕様は次のとおりです。

反対票を投じる:

[Subject(typeof(SomeController))]
public class When_user_clicks_the_vote_down_button_on_a_post : SomeControllerContext
{
    Establish context = () =>
    {
        post = PostFakes.VanillaPost();
        post.Votes = 10;

        session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post);
        session.Setup(s => s.CommitChanges());
    };

    Because of = () => result = controller.VoteDown(1);

    It should_decrement_the_votes_of_the_post_by_1 = () => suggestion.Votes.ShouldEqual(9);
    It should_not_let_the_user_vote_more_than_once;
}

投票:

[Subject(typeof(SomeController))]
public class When_user_clicks_the_vote_down_button_on_a_post : SomeControllerContext
{
    Establish context = () =>
    {
        post = PostFakes.VanillaPost();
        post.Votes = 0;

        session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post);
        session.Setup(s => s.CommitChanges());
    };

    Because of = () => result = controller.VoteUp(1);

    It should_increment_the_votes_of_the_post_by_1 = () => suggestion.Votes.ShouldEqual(1);
    It should_not_let_the_user_vote_more_than_once;
}

だから私は2つの質問があります:

  1. これら 2 つの仕様を DRY するにはどうすればよいですか? コントローラーアクションごとに1つの仕様を実際に持つべきですか? 私は通常そうすべきだと知っていますが、これは自分自身を何度も繰り返しているように感じます.

  2. It同じ仕様内で2番目を実装する方法はありますか? には、 2 回It should_not_let_the_user_vote_more_than_once;呼び出す仕様が必要であることに注意してください。個別の仕様を作成するのが最も簡単なのはわかっていますが、同じコードをもう一度controller.VoteDown(1)コピーして貼り付ける必要があります...

私はまだ BDD (および MSpec) のコツをつかんでいますが、多くの場合、どちらに進むべきか、または BDD のベスト プラクティスやガイドラインが何であるかが明確ではありません。どんな助けでも大歓迎です。

4

3 に答える 3

8

2 番目の質問から始めましょう。MSpec にはItフィールドの複製に役立つ機能がありますが、このシナリオでは使用しないことをお勧めします。この機能は Behaviors と呼ばれ、次のようになります。

[Subject(typeof(SomeController))]
public class When_user_clicks_the_vote_up_button_on_a_post : SomeControllerContext
{
    // Establish and Because cut for brevity.

    It should_increment_the_votes_of_the_post_by_1 =
        () => suggestion.Votes.ShouldEqual(1);

    Behaves_like<SingleVotingBehavior> a_single_vote;
}

[Subject(typeof(SomeController))]
public class When_user_clicks_the_vote_down_button_on_a_post : SomeControllerContext
{
    // Establish and Because cut for brevity.

    It should_decrement_the_votes_of_the_post_by_1 = 
        () => suggestion.Votes.ShouldEqual(9);

    Behaves_like<SingleVotingBehavior> a_single_vote;
}

[Behaviors]
public class SingleVotingBehavior
{
    It should_not_let_the_user_vote_more_than_once =
        () => true.ShouldBeTrue();
}

動作クラスでアサートするフィールドは、動作クラスprotected staticとコンテキスト クラスの両方にある必要があります。MSpec ソース コードには、別の例が含まれています。

あなたの例には実際には4つのコンテキストが含まれているため、動作を使用しないことをお勧めします。「ビジネス上の意味」という観点から、コードで何を表現しようとしているのかを考えると、次の 4 つのケースが浮かび上がります。

  • ユーザーが初めて賛成票を投じる
  • ユーザーが初めて反対票を投じる
  • ユーザーが 2 回目の賛成票を投じる
  • ユーザーが 2 回目の反対票を投じる

4 つの異なるシナリオのそれぞれについて、システムがどのように動作するかを詳細に説明する個別のコンテキストを作成します。4 つのコンテキスト クラスは多くの重複コードであり、これが最初の質問につながります。

以下の「テンプレート」には、呼び出したときに何が起こるかを説明する名前を持つメソッドを持つ基本クラスが 1 つあります。Becauseしたがって、MSpec が「継承された」フィールドを自動的に呼び出すという事実に頼る代わりに、コンテキストにとって重要なものに関する情報をEstablish. 私の経験からすると、これは後でスペックが失敗した場合に備えて読むときに大いに役立ちます。クラス階層をナビゲートする代わりに、行われるセットアップの感触をすぐに得ることができます。

関連する注意事項として、2 つ目の利点は、特定のセットアップを使用して派生するさまざまなコンテキストがいくつあっても、基本クラスは 1 つしか必要ないことです。

public abstract class VotingSpecs
{
    protected static Post CreatePostWithNumberOfVotes(int votes)
    {
        var post = PostFakes.VanillaPost();
        post.Votes = votes;
        return post;
    }

    protected static Controller CreateVotingController()
    {
        // ...
    }

    protected static void TheCurrentUserVotedUpFor(Post post)
    {
        // ...
    }
}

[Subject(typeof(SomeController), "upvoting")]
public class When_a_user_clicks_the_vote_up_button_on_a_post : VotingSpecs
{
    static Post Post;
    static Controller Controller;
    static Result Result ;

    Establish context = () =>
    {
        Post = CreatePostWithNumberOfVotes(0);

        Controller = CreateVotingController();
    };

    Because of = () => { Result = Controller.VoteUp(1); };

    It should_increment_the_votes_of_the_post_by_1 =
        () => Post.Votes.ShouldEqual(1);
}


[Subject(typeof(SomeController), "upvoting")]
public class When_a_user_repeatedly_clicks_the_vote_up_button_on_a_post : VotingSpecs
{
    static Post Post;
    static Controller Controller;
    static Result Result ;

    Establish context = () =>
    {
        Post = CreatePostWithNumberOfVotes(1);
        TheCurrentUserVotedUpFor(Post);

        Controller = CreateVotingController();
    };

    Because of = () => { Result = Controller.VoteUp(1); };

    It should_not_increment_the_votes_of_the_post_by_1 =
        () => Post.Votes.ShouldEqual(1);
}

// Repeat for VoteDown().
于 2010-05-16T00:54:31.913 に答える
1

@トーマス・リッケン、

私も MSpec の第一人者ではありませんが、(まだ限定的な) MSpec での実際の経験から、次のような方向に進んでいます。

public abstract class SomeControllerContext
{
    protected static SomeController controller;
    protected static User user;
    protected static ActionResult result;
    protected static Mock<ISession> session;
    protected static Post post;

    Establish context = () =>
    {
        session = new Mock<ISession>();
            // some more code
    }
}

/* many other specs based on SomeControllerContext here */

[Subject(typeof(SomeController))]
public abstract class VoteSetup : SomeControllerContext
{
    Establish context = () =>
    {
        post= PostFakes.VanillaPost();

        session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post);
        session.Setup(s => s.CommitChanges());
    };
}

[Subject(typeof(SomeController))]
public class When_user_clicks_the_vote_up_button_on_a_post : VoteSetup
{
    Because of = () => result = controller.VoteUp(1);

    It should_increment_the_votes_of_the_post_by_1 = () => post.Votes.ShouldEqual(11);
    It should_not_let_the_user_vote_more_than_once;
}

[Subject(typeof(SomeController))]
public class When_user_clicks_the_vote_down_button_on_a_post : VoteSetup
{
    Because of = () => result = controller.VoteDown(1);

    It should_decrement_the_votes_of_the_post_by_1 = () => post.Votes.ShouldEqual(9);
    It should_not_let_the_user_vote_more_than_once;
}

これは基本的に私がすでに持っていたものですが、あなたの答えに基づいて変更を加えています(私はVoteSetupクラスを持っていませんでした.)

あなたの答えは私を正しい方向に導きました。この件に関する他の視点を集めるために、さらにいくつかの回答を期待しています... :)

于 2010-05-14T18:38:41.920 に答える
0

テストのセットアップを除外するだけで、繰り返しの多くを除外できる可能性があります。賛成票の仕様が 10 票から 11 票ではなく 0 票から 1 票になるべき本当の理由はないので、1 つのセットアップ ルーチンを持つことができます。それだけで、両方のテストが 3 行のコード (セットアップ メソッドを手動で呼び出す必要がある場合は 4 行) になります。

突然、テストはアクションの実行と結果の検証のみで構成されます。反復的であるかどうかにかかわらず、テストごとに 1 つのことをテストすることを強くお勧めします。これは、1 か月で何かをリファクタリングし、ソリューションですべてのテストを実行するときに、テストが失敗する理由を正確に知りたいからです。

更新(詳細についてはコメントを参照してください)

private WhateverTheTypeNeedsToBe vote_count_context = () => 
{
    post = PostFakes.VanillaPost();
    post.Votes = 10;

    session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post);
    session.Setup(s => s.CommitChanges());
};

そしてあなたの仕様では:

Establish context = vote_count_context;
...

これは機能しますか?

于 2010-05-14T13:40:55.533 に答える