Follow @RoyOsherove on Twitter

Scala, NRehersal and Fluent Test Interfaces

This post by Ian cooper triggered my interest in Scala, a new object oriented, functional, programming language that is very accessible for java developers.  From the looks of it, it seems very accessible for C# developers as well.

Scouring around related links for a while I came across the Rehersal project, which is a unit testing framework for Scala(which already has a built in SUnit test framework very muich like JUnit), that tries to make things "less cumbersome" for developers, such as giving test names with spaces, etc.

Here's an example of a Rehersal test class:

package examplesimport rehersal._import rehersal.TestCategories._class SkeletonTests extends Tests(UnitTesting){
 test("Example of a test that will fail")( ()=> { fail() })
 test("Example of expected exceptions that will pass", classOf[IndexOutOfBoundsException])( ()=> { throw new IndexOutOfBoundsException() })
 
//and this is how you implement test setup and teardown :
 onceBefore = () => { note("Run once before all tests in this object") } onceAfter = () => { note("Run once after all tests in this object, unless onceBefore failed") } before = () => { note("Run before each test unless the test is a duplicate") } after = () => { note("Run after each test, unless before failed") }
}

 

At first look I thought it looked a lot like lambda expressions in C# 3.5 so I went ahead a did a little implementation of something I call NRehersal, which tries to mimic the same style of the Rehersal framework.

As I was working on it it occured to me that what's really interesting to me is the fact that the tests, if implemented as regular method calls, can be implemented in a more fluent syntax, so I came up with something that looks like this:

public class NRehersalTestSimple : RehersalBase
{public override void TheTests()
    {
        onceBeforeEachTest=delegate
            {
                Console.WriteLine("before each TEST");
            };

        onceBeforeAllTests=delegate
            {
                Console.WriteLine("before all tests");
            };

        TEST("This is a TEST", delegate
        {
            Console.WriteLine("in TEST");
        });

        TEST("This is a TEST with an expected exception")
            .ExpectException<OutOfMemoryException>()
            .Execute(delegate
                         {
                             throw new OutOfMemoryException("whatever");
                         });
    }}

 

Things to note here:

  • There is one big method called 'TheTests" that we override
  • We call "test" for each test case.
  • that means all tests will always run. you can't run just part of the tests by default.
  • setup and teardown are implemented by replacing delegates on the base class
  • you could use lambda syntax instead of the word delegate ( ()=> )

 

Extensibility

But then I got to thinking, how extensibility can be taken care of, for example. what happens if I want to add a repeatable test ability?

This is achieved using the .NET 3.5 features: an Extension method to the interface returned by the test method, and a customized delegate that can be changed at runtime for the test runner itself. here is what it would look like:

  • Create a custom class that inherits from TestData that will implement new features  (a Times(int) method)

internal class RepeatSupportingTestData : TestData
   {
       private int timesToRun = 2;

       public int TimesToRun
       {
           get { return timesToRun; }
           set { timesToRun = value; }
       }

       public RepeatSupportingTestData(TestData data)
           : base(data.Code,data.Name)
       {
       }
   }

 

 

  • Add an extension to the interface named ITestInvocation so you can use this new feature in your tests

static class TestExtensions
{
    public static ITestInvocation Times(this ITestInvocation exp, int time)
    {
        RepeatSupportingTestData data = exp.Data as RepeatSupportingTestData;
        data.TimesToRun = time;
        return exp;
    }
}

  • Replace the delegates for creating TestData and for running a test with your own code

public class NRehersalExtensions : RehersalBase
    {
        private static Action<TestData, RehersalBase> originalSingleTestRunner;
        public NRehersalExtensions()
        {
            //save the original single test runner to be run in a loop later
            originalSingleTestRunner = TestOverrides.SingleTestRunMethod;

            //change the single test runner to our own repeat runner
            TestOverrides.SingleTestRunMethod = RepeatTestMethod;
            //change the test data factory to out own so we can return our own test
            //type that adds the repeat time property.
            TestOverrides.TestDataFactory = CreateRepeatableTestData;
        }

        private static TestData CreateRepeatableTestData(TestData data)
        {
            return new RepeatSupportingTestData(data);
        }

        private static void RepeatTestMethod(TestData test, RehersalBase testClass)
        {
            RepeatSupportingTestData data = test as RepeatSupportingTestData;
            for (int i = 0; i < data.TimesToRun; i++)
            {
                originalSingleTestRunner(data, testClass);
            }

        }

        public override void TheTests()
        {

            TEST("repeat TEST").Times(3)
                .Execute(()=>
                             {
                                 Console.WriteLine("Executing repeat TEST");
                             });

        }
    }

 

You can get the code and zip file here at the google project page.

This is just something I threw together to see what it would fee like. it is still not implementing the various Asserts and expectations, which is a trivial matter of adding these methods to the RehersalBase class.

 

Your thoughts are welcome. Personally I'm not sure what I feel about this syntax, but I like the fluent test interface that can be made with it, and the ease of extensibility.

Refactoring private methods is like...?

About the ALT.NET mailing lists