DRY-IN очень похожи спецификации для действия контроллера ASP.NET MVC с MSpec (рекомендации BDD) - PullRequest
5 голосов
/ 14 мая 2010

У меня есть две очень похожие спецификации для двух очень похожих действий контроллера: VoteUp (int id) и VoteDown (int id). Эти методы позволяют пользователю голосовать за сообщение вверх или вниз; вроде как функция голосования вверх / вниз для вопросов StackOverflow. Спецификации:

VoteDown:

[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;
}

VoteUp:

[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;
}

Итак, у меня два вопроса:

  1. Как я могу заняться СУШКОЙ этих двух спецификаций? Это даже желательно, или я должен иметь одну спецификацию на действие контроллера? Я знаю, что обычно должен, но это похоже на многократное повторение.

  2. Есть ли способ реализовать второй It в той же спецификации? Обратите внимание, что It should_not_let_the_user_vote_more_than_once; требует от меня спецификации для вызова controller.VoteDown(1) дважды. Я знаю, что проще всего было бы также создать для него отдельную спецификацию, но это было бы копирование и вставка того же кода еще раз ...

Я все еще изучаю BDD (и MSpec), и часто неясно, каким путем мне следует идти, или каковы лучшие практики или рекомендации для BDD. Любая помощь будет оценена.

Ответы [ 3 ]

8 голосов
/ 16 мая 2010

Начну со второго вопроса: в MSpec есть функция, которая поможет с дублированием полей It, но в этом сценарии я бы посоветовал не использовать ее. Эта функция называется «Поведение» и выглядит примерно так:

[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 содержит другой пример .

Я не советую использовать поведение, потому что ваш пример содержит четыре контекста. Когда я думаю о том, что вы пытаетесь выразить с помощью кода в терминах «делового значения», возникают четыре разных случая:

  • Пользователь голосует впервые
  • Пользователь впервые голосует вниз
  • Пользователь голосует во второй раз
  • Пользователь голосует второй раз

Для каждого из четырех различных сценариев я бы создал отдельный контекст, который подробно описывает, как должна вести себя система. Четыре контекстных класса представляют собой много дублирующегося кода, что приводит нас к первому вопросу.

В приведенном ниже «шаблоне» есть один базовый класс с методами, которые имеют описательные имена того, что произойдет, когда вы их вызовете. Поэтому вместо того, чтобы полагаться на тот факт, что MSpec будет автоматически вызывать «унаследованные» поля Because, вы помещаете информацию о том, что важно для контекста, прямо в Establish. Исходя из моего опыта, это поможет вам намного позже, когда вы прочитаете спецификацию на случай, если она выйдет из строя. Вместо навигации по иерархии классов вы сразу же чувствуете, что происходит.

Что касается связанной заметки, вторым преимуществом является то, что вам нужен только один базовый класс, независимо от того, сколько различных контекстов с определенной настройкой вы получаете.

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().
1 голос
/ 14 мая 2010

@ Томас Ликен,

Я тоже не гуру 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.)

Ваш ответ привел меня в правильном направлении. Я все еще надеюсь получить еще несколько ответов, чтобы собрать другие точки зрения на эту тему ... :)

0 голосов
/ 14 мая 2010

Вы, вероятно, могли бы выделить большую часть повторений, просто выделив setup тестов. Нет никакой реальной причины, по которой спецификация upvote должна идти от 0 до 1 голоса, а не от 10 до 11, поэтому вы вполне можете иметь одну процедуру установки. Одно это оставит оба теста в 3 строках кода (или в 4, если вам нужно вызвать метод установки вручную ...).

Неожиданно ваши тесты состоят только из выполнения действия и проверки результатов. И независимо от того, кажется ли вам это повторением или нет, я настоятельно рекомендую вам проверить одну вещь на тест, просто потому, что вы хотите точно знать, почему тест не проходит, если вы что-то реорганизуете в течение месяца и запускаете все тесты в решении.

ОБНОВЛЕНИЕ (подробности см. В комментариях)

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

Может ли это сработать?

...