简体   繁体   中英

BDD for C# NUnit

I've been using a home brewed BDD Spec extension for writing BDD style tests in NUnit, and I wanted to see what everyone thought. Does it add value? Does is suck? If so why? Is there something better out there?

Here's the source: https://github.com/mjezzi/NSpec

There are two reasons I created this

  1. To make my tests easy to read.
  2. To produce a plain english output to review specs.

Here's an example of how a test will look:

-since zombies seem to be popular these days..

Given a Zombie, Peson, and IWeapon:

namespace Project.Tests.PersonVsZombie
{
    public class Zombie
    {

    }

    public interface IWeapon
    {
        void UseAgainst( Zombie zombie );
    }

    public class Person
    {
        private IWeapon _weapon;

        public bool IsStillAlive { get; set; }

        public Person( IWeapon weapon )
        {
            IsStillAlive = true;
            _weapon = weapon;
        }

        public void Attack( Zombie zombie )
        {
            if( _weapon != null )
                _weapon.UseAgainst( zombie );
            else
                IsStillAlive = false;
        }
    }
}

And the NSpec styled tests:

public class PersonAttacksZombieTests
{
    [Test]
    public void When_a_person_with_a_weapon_attacks_a_zombie()
    {
        var zombie = new Zombie();
        var weaponMock = new Mock<IWeapon>();
        var person = new Person( weaponMock.Object );

        person.Attack( zombie );

        "It should use the weapon against the zombie".ProveBy( spec =>
            weaponMock.Verify( x => x.UseAgainst( zombie ), spec ) );

        "It should keep the person alive".ProveBy( spec =>
            Assert.That( person.IsStillAlive, Is.True, spec ) );
    }

    [Test]
    public void When_a_person_without_a_weapon_attacks_a_zombie()
    {
        var zombie = new Zombie();
        var person = new Person( null );

        person.Attack( zombie );

        "It should cause the person to die".ProveBy( spec =>
            Assert.That( person.IsStillAlive, Is.False, spec ) );
    }
}

You'll get the Spec output in the output window:

[PersonVsZombie]

- PersonAttacksZombieTests

    When a person with a weapon attacks a zombie
        It should use the weapon against the zombie
        It should keep the person alive

    When a person without a weapon attacks a zombie
        It should cause the person to die

2 passed, 0 failed, 0 skipped, took 0.39 seconds (NUnit 2.5.5).

I'm going to call out some uses of BDD rather than just the framework, as I think having a really great understanding of unit-level BDD might affect some of the things you create. Overall, I like it. Here goes:

Rather than calling them PersonAttacksZombieTests , I'd just call them PersonTests or even PersonBehaviour . It makes it much easier to find the examples associated with a particular class this way, letting you use them as documentation.

It doesn't look like IsStillAlive is the kind of thing you'd want to set on a person; rather an intrinsic property. Careful making things like this public. You're adding behaviour that you don't need.

Calling new Person(null) doesn't seem particularly intuitive. If I wanted to create a person without a weapon, I would normally look for a constructor new Person() . A good trick with BDD is to write the API you want, then make the code underneath do the hard work - make code easy to use, rather than easy to write.

The behaviour and responsibilities also seem a bit odd to me. Why is the person, and not the zombie, responsible for determining whether the person lives or dies? I'd prefer to see behaviour like this:

  • A person can be equipped with a weapon (via person.Equip(IWeapon weapon) ).
  • A person starts with a fist if they have no weapon.
  • When the person attacks a zombie, the person uses the weapon on the zombie.
  • The weapon determines whether the zombie lives or dies.
  • If the zombie is still alive, it attacks back. The zombie will kill the person (via person.Kill ).

That seems to me as if it's got the behaviour and responsibilities in a better place. Using a different kind of weapon for useless attacks, rather than checking for null, also allows you to avoid that if statement. You'd need different tests:

  • A fist should not kill a zombie when used against it
  • A chainsaw should kill a zombie when used against it
  • A person should use their equipped weapon when attacking a zombie
  • A person should be equipped with a fist if they have no other weapon
  • A zombie should attack back when it's still alive.
  • A zombie should not attack back if it's dead.
  • A zombie should die if killed.
  • A person should die if killed.

Other than that, it looks great. I like the way you've used the mocks, the flow of strings, and the phrasing of the test methods themselves. I also quite like ProveBy ; it's doing exactly what it says on the tin, and nicely ties up the difference between providing examples of behaviour and running them as tests.

I've been giving this sort of question a lot of though recently. There are a lot of reasonable options out there, and you can create your own easily, as displayed in some of the answers in this post. I've been working on a BDD testing framework with the intent being to make it easily extended to any unit testing framework. I currently support MSTest and NUnit. Its called Given , and it's opensource. The basic idea is pretty simple, Given provides wrappers for common sets of functionality which can then be implemented for each test runner.

The following is an example of an NUnit Given test:

[Story(AsA = "car manufacturer",
       IWant = "a factory that makes the right cars",
       SoThat = "I can make money")]
public class when_building_a_toyota : Specification
{
    static CarFactory _factory;
    static Car _car;

    given a_car_factory = () =>
                              {
                                  _factory = new CarFactory();
                              };

    when building_a_toyota = () => _car = _factory.Make(CarType.Toyota);

    [then]
    public void it_should_create_a_car()
    {
        _car.ShouldNotBeNull();
    }

    [then]
    public void it_should_be_the_right_type_of_car()
    {
        _car.Type.ShouldEqual(CarType.Toyota);
    }
}

I tried my best to stay true to the concepts from Dan North's Introducting BDD blog, and as such, everything is done using the given, when, then style of specification. The way it is implemented allows you to have multiple givens and even multiple when's, and they should be executed in order (still checking into this).

Additionally, there is a full suite of Should extensions included directly in Given. This enables things like the ShouldEqual() call seen above, but is full of nice methods for collection comparison and type comparison, etc. For those of you familiar with MSpec, i basically ripped them out and made some modifications to make them work outside of MSpec.

The payoff, though, I think, is in the reporting. The test runner is filled with the scenario you've created, so that at a glance you can get details about what each test is actually doing without diving into the code: 测试跑步者

Additionally, an HTML report is created using t4 templating based on the results of the tests for each assembly. Classes with matching stories are all nested together, and each scenario name is printed for quick reference. For the above tests the report would look like this: 报告示例

Failed tests would be colored red and can be clicked to view the exception details.

That's pretty much it. I'm using it in several projects I'm working on, so it is still being actively developed, but I'd describe the core as pretty stable. I'm looking at a way to share contexts by composition instead of inheritance, so that will likely be one of the next changes coming down the pike. Bring on the criticism. :)

My issue with this is having "something".ProveBy() does not match the text being displayed later ("When ... it should ..."). I think the concept of BDD is to keep the test wording and the test report as similar as possible.

Try this one,

UBADDAS - User Behaviour and Domain Driven Acceptance Stories

found here - http://kernowcode.github.io/UBADDAS/

It produces console output like this

I want to register a new user 
  So that Increase customer base
       As user
    Given Register customer
     When Confirm customer registration
     Then Login customer

您还可以查看小型库: https//www.nuget.org/packages/Heleonix.Testing.NUnit/您可以在Given / When / Then或Arrange / Act / Assert样式中描述测试。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM