Sunday, January 22, 2012

More Readable tests using SpecificationFor<T>

Introduction

In my current role at an agile company we work in pairs and following XP principles, one of which is TDD. I’ve been following TDD for a few years now and one thing I think that helps me when I come back to old tests and helps other developers who are new to a project is when a test has a "context".

Example of a test with context

class When correcting a customers address with no postcode Then ???

Then what? Well that’s the great thing about working in a pair you get to discuss what should happen in the above "context". Perhaps it should log an error, perhaps it should throw an exception, perhaps it should just do nothing? Who knows but at least the context is in the test.

Another example of a test with context

class When correcting a customers address with a valid address Then ???

Again the name of the class should help kick start the discussion between the pair.

If you compare that to a class without a context:

Example of test without context

class CustomerFixtures Then ???

It’s not quite the same and doesn’t really lend it’s self to discussion about behaviour, what do you think?

I don't want to pretend I invented any of these concepts. They've been around for a while now, it's called BDD or Behaviour driven design. For me though it's more about making your tests readable and more importantly understandable from just looking at the class and method names. Do a search for "Context Specification" on your favourite search engine and you'll find lots more examples.

Code

Enough talking get with the code. Here are 2 small helper classes that have been adapted from a “Specification Context” example I found on the interwebs. This works with NUnit. I hope you’ll find it useful:

public abstract class Specification
{
 [SetUp]
 public void BaseSetUp()
 {
  SetupParameters();
  SetupDependencies();
  InitializeClassUnderTest();
  Because();
 }

 [TearDown]
 public void BaseTearDown()
 {
  DisposeContext();
 }

 protected virtual void SetupParameters() { }
 protected virtual void SetupDependencies() { }
 protected virtual void InitializeClassUnderTest() { }
 protected virtual void Because() { }
 protected virtual void DisposeContext() { }
}
public abstract class SpecificationFor<TClassUnderTest> : Specification
{
 protected override void InitializeClassUnderTest()
 {
  ClassUnderTest = CreateClassUnderTest();
 }

 protected abstract TClassUnderTest CreateClassUnderTest();

 protected TClassUnderTest ClassUnderTest { get; private set; }
}

To show how you can use these helper classes I'm using 3 classes:

  • SpecificationForCorrectCustomerAddress - base class which sets up the class under tests and any dependecies
  • When_correcting_a_customers_address_with_a_valid_address - First context (Happy path)
  • When_correcting_a_customers_address_with_no_postcode - Second context (Unhappy path)
public abstract class SpecificationForCorrectCustomerAddress: SpecificationFor<Customer>
{
    protected Customer.AddressDetails AddressDetails;
    protected ILogger Logger;

    protected override void SetupParameters()
    {
        AddressDetails = new Customer.AddressDetails { Postcode = "AB1 2CD" };
    }

    protected override void SetupDependencies()
    {
        Logger = Substitute.For<ILogger>();
    }

    protected override Customer CreateClassUnderTest()
    {
        return new Customer(Logger);
    }

    protected override void Because()
    {
        ClassUnderTest.CorrectCustomerAddress(AddressDetails);
    }
}
[TestFixture]
public class When_correcting_a_customers_address_with_a_valid_address : SpecificationForCorrectCustomerAddress
{
    [Test]
    public void Then_correction_is_logged()
    {
        Logger.Received().Info(Arg.Any<string>());
    }
}
[TestFixture]
public class When_correcting_a_customers_address_with_no_postcode : SpecificationForCorrectCustomerAddress
{
    private Action action;

    protected override void SetupParameters()
    {
        AddressDetails = new Customer.AddressDetails();
    }

    protected override void Because()
    {
        action = () => ClassUnderTest.CorrectCustomerAddress(AddressDetails);
    }

    [Test]
    public void Then_unknown_postcode_exception_is_thrown()
    {
        action.ShouldThrow();
    }
}

As you can see all you do is inherit from the SpecificationFor<T> abstract class and implement the CreateClassUnderTest method. The other methods are optional. I like to split up my setup process into the following parts:

  • SetupParameters - an area to setup any parameters required by method/property being called
  • SetupDependencies- an area to setup any dependencies required by ClassUnderTest
  • CreateClassUnderTest - This is where you create the ClassUnderTest, passing it any dependencies it might require.
  • Because- This is where you call the method/property on the ClassUnderTest
  • DisposeContext- Any Tidying up code you might require

This pattern is classic TDD, I.e. Arrange, Act, Assert. Where SetupParameters, SetupDependencies and CreateClassUnderTest are part of the Arrange. Because is the Act and the [Test] methods are just the Assert. This results in a very readable output in your favourite test runner.

What the tests look like in a resharper test session

Conclusion

I think that the output alone is enough to explain what happens when correcting a customer's address. As I said in the beginning, it helps me when I come back to tests. If you want to download the example there is a copy on my git hub account here.

22 comments:

  1. Good article. I find this approach very comfortable to use for tests. Thanks!

    P.S. Looks very nice in a ReSharper test session :)

    ReplyDelete
  2. Thanks Siarhei,

    Really chuffed that you took the time to download the example and check it out.. happy testing.

    ReplyDelete
  3. Muy bien! Found it tricky at first because the inheritance hides stuff whereas in the old flat style of writing tests the code was all in the same place. I see that starting from the Because() is the best place to start when debugging your tests or getting a little more understanding of the code under test.

    ReplyDelete
  4. Hal yang paling membuat para penjudi kalah adalah emosi yang buruk saat bermain. Dengan begitu maka para penjudi jangan sampai untuk bermain dalam emosi yang buruk.
    asikqq
    http://dewaqqq.club/
    http://sumoqq.today/
    interqq
    pionpoker
    bandar ceme terpercaya
    freebet tanpa deposit
    paito warna
    syair sgp

    ReplyDelete

  5. fantastic post from you guys that amazed me as I get lot to learn from your article. It is Really very informative and creative contents for all the person who is looking to gain knowledge from the article. Thanks for sharing in information
    Thanks.
    DedicatedHosting4u.com

    ReplyDelete
  6. It is amazing and wonderful to visit your site.Thanks for sharing this information,this is useful to me...Your good knowledge and kindness in playing with all the pieces were very useful. I don’t know what I would have done if I had not encountered such a step like this.
    Selenium online training
    Selenium certification training
    Selenium online course
    Selenium training course

    ReplyDelete
  7. It is amazing and wonderful to visit your site.Thanks for sharing this information,this is useful to me...Your good knowledge and kindness in playing with all the pieces were very useful. I don’t know what I would have done if I had not encountered such a step like this.
    Docker online training
    Docker certification training
    Docker online course
    Docker training course

    ReplyDelete
  8. Wonderful bloggers like yourself who would positively reply encouraged me to be more open and engaging in commenting.So know it's helpful.
    Software testing online training
    Software testing certification training
    Software testing online course
    Software testing training course

    ReplyDelete
  9. Nice post. By reading your blog, i get inspired and this provides some useful information. Thank you for posting this exclusive post for our vision.
    Selenium online training
    Selenium certification training
    Selenium online course
    Selenium training course

    ReplyDelete
  10. Wonderful bloggers like yourself who would positively reply encouraged me to be more open and engaging in commenting.So know it's helpful. Blockchain online training
    Blockchain certification training
    Blockchain online course
    Blockchain training course

    ReplyDelete
  11. ยูฟ่าเบท หรือ UFABET คือ เว็บให้บริการเดิมพันออนไลน์แบบครบวงจรโดยไม่ผ่านเอเย่นต์หรือตัวแทนใดๆ ซึ่งมีเกมและกีฬาให้เลือกเดิมพันมากมายหลากหลายประเภท รวมไปถึงคาสิโนออนไลน์หลายชนิด UFABET ถือได้ว่าเป็นเว็บไซต์ให้บริการเดิมพันออนไลน์ที่ผู้คนส่วนใหญ่นิยมใช้บริการ เพราะด้วยรูปแบบเว็บไซต์ที่ใช้งานง่ายและไม่จำเป็นต้องทำความเข้าใจอะไรเยอะ ประกอบกับเว็บนี้ยังรองรับการใช้งานในหลายๆภาษา ทั้ง จีน ฮ่องกง ไทย อังกฤษฯ จึงทำให้ผู้ใช้งานส่วนใหญ่ทั้งหน้าใหม่หรือหน้าเก่าต่างเลือกที่จะหันมาใช้บริการกับทาง ยูฟ่าเบท มากขึ้น จึงเป็นเว็บเดิมพันออนไลน์ที่ดีที่สุดในภูมิภาคเอเชีย ufa

    ReplyDelete
  12. Every call and enquiry bodyguard servicesthat we receive is dealt with the utmost discretion, respect, carefulness and understanding.

    ReplyDelete
  13. You have posted such a fabulous post; I am looking for such type of helpful posts. Thanks for sharing it. Kinemaster Gold

    ReplyDelete
  14. Null’s Brawl APK is a modified version which have unlimited money and unlocked all skins for free.

    ReplyDelete
  15. Mechanical Project in Coimbatore

    Mechanical Autocad Training

    Autocad Mechanical Course

    Autocad Online Course for Mechanical

    Autocad Course for Mechanical Engineering

    Cadd Center Courses for Mechanical

    Autocad for Mechanical Engineering Courses

    Mechanical Cad Training

    Cad Course for Mechanical Engineer

    Cadd Center Courses for Mechanical Fees

    ReplyDelete
  16. Nice image visibility and well written blog. Thanks for sharing.

    ReplyDelete