mardi 26 septembre 2017

Dynamic builder pattern for test fixture workflows

I am writing an automated testing framework for a web application in which the central domain object is shared between users through different independent workflows. These workflows generate data elsewhere in the system, leaving a wake of data that references the domain object, and only in some cases update properties on that object.

It is impractical and redundant to automate a user's actions on the front-end through the same workflow more than once to test some number of follow-up actions after that workflow has been executed.

In order to create data fixtures to run my tests, I would like to implement something like a builder pattern to build both the domain object and the "wake" of data that would result from following some workflow involving that object.

However, I would like to limit the callable methods on the builder in such a way that corresponds to the business logic of the system, such that only methods that make sense from a business perspective could be chained together. I am thinking of creating an extension method that casts the underlying object to some subclass which implements an interface that defines the methods on that workflow, but I'm not sure if this is possible, or even a decent way to solve the problem.

In the example below, if I am building something spoon-like in my workflow, I should not be able to call DrillButtonHoles(). Doing so should modify the object being built into something "ButtonLike," and upon which only methods pertaining to the "Button workflow" can be called. Likewise, if I am building something button-like in my workflow, I should not be able to call SandSpoonHandle().

The ProductBuilder class:

public class ProductBuilder
    {
        private BlockOfWood _blockOfWood;

        public ProductBuilder(int blockNumber)
        {
            _blockOfWood.BlockNumber = blockNumber;
        }

        public ProductBuilder Create()
        {
            _blockOfWood = new BlockOfWood();
            return this;
        }

        public ProductBuilder WithBuildStart(DateTime buildStart)
        {
            _blockOfWood.OperationsStarted = buildStart;
            return this;
        }

        public ProductBuilder AddButtonHoles(int numberOfHoles)
        {
            // The methods here create data with FK to the blockNumber.
            DrillingOperations.DrillButtonHoles(_blockOfWood.BlockNumber, numberOfHoles);
            // ...
            // save changes to database
            // Here the type of Product should change 
            // to something like Button : BlockOfWood 
            return this;
        }

        public ProductBuilder SandSpoonHandle(string grit)
        {
            // The methods here create data with FK to the blockNumber.
            SandingOperations.SandSpoon(_blockOfWood.BlockNumber, grit);

            // save changes to database
            // Here the type of Product should change 
            // to something like Spoon : BlockOfWood 
            return this;
        }
    }

Some domain object:

public class BlockOfWood
{
    public int BlockNumber { get; set; }
    public DateTime OperationsStarted { get; set; }
    // ...
}

Some test requiring a specific workflow to have been executed:

    public void User_Can_Eat_Cereal()
    {
        // I can do this:
        var spoon = new ProductBuilder(123);
        spoon.SandSpoonHandle("Fine");

        // but I wouldn't like this to be possible:
        spoon.SandSpoonHandle("Fine").AddButtonHoles(2);

        // Some action
        // Some assertion
    }
}

Aucun commentaire:

Enregistrer un commentaire