Skip to content

Add Replacement Capabilities #410

@JasonBock

Description

@JasonBock

Describe the solution you'd like

Replacing Expectations

Let's say you set up a test like this:

using NUnit.Framework;

public sealed class MyTests
{
    private RockContext context;
    private ICustomerCreateExpectations customerExpectations;

    [SetUp]
    public void SetUp()
    {
        this.context = new();
        this.customerExpectations = this.context.Create<ICustomerCreateExpectations>();
        this.customerExpectations.Setups.Retrieve(123).ReturnValue(new Customer("Jane"));
    }

    [Test]
    public void GetCustomer()
    {
        var customerService = new CustomerService(this.customerExpectations.Instance());
        // Do something with customerService where Retrieve() is called with 123...
    }

    [TearDown]
    public void TearDown()
    {
        this.context.Dispose();
    }
}

This example uses NUnit, but the test framework is irrelevant. The point is, you have a scenario where you set up expectations in a separate method that the test will use.

Now, maybe setting up Retrieve(123) is needed like this for 20 tests in this class, but one test wants to change the return value. The test code could be changed like this:

using NUnit.Framework;

public sealed class MyTests
{
    private RockContext context;
    private ICustomerCreateExpectations customerExpectations;
    private AdornmentsForHandler0 customerRetrieveAdornments;

    [SetUp]
    public void SetUp()
    {
        this.context = new();
        this.customerExpectations = this.context.Create<ICustomerCreateExpectations>();
        this.customerRetrieveAdornments = this.customerExpectations.Setups.Retrieve(123).ReturnValue(new Customer("Jane"));
    }

    [Test]
    public void GetCustomer()
    {
        var customerService = new CustomerService(this.customerExpectations.Instance());
        // Do something with customerService where Retrieve() is called with 123...
    }


    [Test]
    public void GetCustomerDifferent()
    {
        this.customerRetrieveAdornments.ReturnValue(new Customer("Joe"));
        var customerService = new CustomerService(this.customerExpectations.Instance());
        // Do something with customerService where Retrieve() is called with 123...
    }

    [TearDown]
    public void TearDown()
    {
        this.context.Dispose();
    }
}

OK, but what if the whole expectations should be replaced? Right now, that can't be done in Rocks, but maybe...I add support for that:

using NUnit.Framework;

public sealed class MyTests
{
    private RockContext context;
    private ICustomerCreateExpectations customerExpectations;
    private AdornmentsForHandler0 customerRetrieveAdornments;

    [SetUp]
    public void SetUp()
    {
        this.context = new();
        this.customerExpectations = this.context.Create<ICustomerCreateExpectations>();
        this.customerRetrieveAdornments = this.customerExpectations.Setups.Retrieve(123).ReturnValue(new Customer("Jane"));
    }

    [Test]
    public void GetCustomer()
    {
        var customerService = new CustomerService(this.customerExpectations.Instance());
        // Do something with customerService where Retrieve() is called with 123...
    }


    [Test]
    public void GetCustomerDifferent()
    {
        this.customerRetrieveAdornments = this.customerExpectations.Replace(
            this.customerRetrieveAdornments,
            this.customerExpectations.Setups.Retrieve(456).ReturnValue(new Customer("Joe")));
        var customerService = new CustomerService(this.customerExpectations.Instance());
        // Do something with customerService where Retrieve() is called with 123...
    }

    [TearDown]
    public void TearDown()
    {
        this.context.Dispose();
    }
}

So...what if I want to replace the entire expectation object? This really only makes sense if you're using RockContext:

using NUnit.Framework;

public sealed class MyTests
{
    private RockContext context;
    private ICustomerCreateExpectations customerExpectations;

    [SetUp]
    public void SetUp()
    {
        this.context = new();
        this.customerExpectations = this.context.Create<ICustomerCreateExpectations>();
        this.customerExpectations.Setups.Retrieve(123).ReturnValue(new Customer("Jane"));
    }

    [Test]
    public void GetCustomer()
    {
        var customerService = new CustomerService(this.customerExpectations.Instance());
        // Do something with customerService where Retrieve() is called with 123...
    }

    [Test]
    public void GetCustomerDifferent()
    {
        this.customerExpectations = this.context.Replace(this.customerExpectations);
        this.customerExpectations.Setups.Delete(456).ReturnValue(new Customer("Jeff"));
        var customerService = new CustomerService(this.customerExpectations.Instance());
        // Do something with customerService where Remove() is called with 456...
    }

    [TearDown]
    public void TearDown()
    {
        this.context.Dispose();
    }
}

Having APIs in place to do these kinds of replacements allow the developer to set up common expectation code that will be shared by multiple tests, making small changes in specific tests where needed.

So, essentially two replacement features should be added:

  • Add a TAdornments Replace<TAdornments>(TAdornments adornments, TAdornments newAdornments) where TAdornments : Adornments. Not sure if this would go on the generated expectations type, or if it can be put on Expectations itself. I'm guessing the former, and it feels like it may be a decent amount of Reflection to find which handler it is, though I'm not sure of that.
  • Add a T Replace<T>(T expectations) where T : Expectations, new() to RockContext. This would remove the expectations instance in the context and create a new one based on the given T type.

Nothing needs to be done to change the actual adornments for an expectation. That is, a developer can call ExpectedCallCount(), Callback(), and ReturnValue() multiple times if they want, and subsequent calls will replace the current value.

Also, one thing to consider with this change: RockContext is not thread-safe. It was never designed to be thread-safe, and, frankly, I don't think it should be. If tests are set up to share a context, things may break unexpectedly if the tests themselves run in parallel. Ultimately, I think it would be the responsibility of the developer to manage shared context instances such that race conditions would not occur. Note that different testing frameworks handle parallelism differently - again, it's the developer's choice on how to handle this scenario.

Describe alternatives you've considered
Keep things as-is.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions