domn1995/dunet

Add Action with the type in the else of a MatchXYZ

JoanComasFdz opened this issue · 7 comments

I am trying Dunet in a very simple piece of code.

I want to write unit tests where I say: If the returned type is correct, assert some properties, otherwise fail saying that the wrong options was returned.

My DU is:

Union]
internal partial record LogChange
{
    partial record AddNew(Disconnection NewDisconnection);
    partial record UpdateLastEndTime(Disconnection Last, DateTime EndTime);
    partial record UpdateLastEndTimeAndAddNew(Disconnection Last, DateTime EndTime, Disconnection NewDisconnection);
}

So I would like to write the unit test as follows:

var result = BusinessLogic.DetermineLogChanges(e, null);

result.MatchAddNew(
    addNew =>
    {
        Assert.Equal(expectedHardwarUnitId, addNew.NewDisconnection.HardwareUnitId);
        Assert.Equal(expectedState, addNew.NewDisconnection.State);
    },
    // The else should be of the other type that was returned
    @else => Assert.Fail($"Returned {@else} instead of {nameof(BusinessLogic.AddNew)}")
    );

Unfortunately the MatchAddNew() method only has an Action without input parameter as else.

Is it possible to add an Action as an else, where T is the actual DU value returned?

Thanks for taking the time to submit your feedback!

This seems like the perfect use case for the Unwrapping feature. For example:

var result = BusinessLogic.DetermineLogChanges(e, null);
var addNew = result.UnwrapAddNew();

Assert.Equal(expectedHardwarUnitId, addNew.NewDisconnection.HardwareUnitId);
Assert.Equal(expectedState, addNew.NewDisconnection.State);

Let me know if this works for you!

Ok I followed your recommendation.

This is my unit test now:

[Fact]
public void DetermineLogChanges_LastDoesntExist_CreatesNew()
{
    var expectedHardwarUnitId = "u1";
    var expectedState = HardwareConnectionState.DISCONNECTED;
    var expectedStartTime = DateTime.Now;
    var e = new HardwareConnectionStateChangedEvent(expectedHardwarUnitId, expectedState, expectedStartTime);

    var result = BusinessLogic.DetermineLogChanges(e, null);

    var addNew = result.UnwrapAddNew();
    Assert.Equal(expectedHardwarUnitId, addNew.NewDisconnection.HardwareUnitId);
    Assert.Equal(expectedState, addNew.NewDisconnection.State);
    Assert.Equal(expectedStartTime, addNew.NewDisconnection.StartTime);
    Assert.Null(addNew.NewDisconnection.EndTime);
}

I think it looks good, it is very readable.

And this is how a failed test looks like:

image

I think it is good enough, although the error talks about the wrong method called, not the wrong type returned.

I am looking for a native equivalent to the methods that OneOf exposes like Assert.True(result.IsT0);. I think this can be improved because IsT0 is extremely technical and talks about the particular implementation of that library.

I could use the normal Match() but then I have to do an Assert.Fail(...) in every other value.

Maybe Action<object>() is not the best approach, although I see that DiscU seems to do something similar:

Color c2 = backgroundColor
   .Match((Color col) => col)
   .Else(obj => /* return default value */) // <- obj is the actual value

Do you see the value?

If not, what about having autogenerated IsAddNew(), IsUpdateLastEndTime(), etc?

It doesn't feel worth adding IsVariant() methods to unions because you can already do something like this natively:

var isAddNew = result is AddNew addNew;
Assert.True(isAddNew);
Assert.Equal(expectedHardwarUnitId, addNew.NewDisconnection.HardwareUnitId);
Assert.Equal(expectedState, addNew.NewDisconnection.State);
Assert.Equal(expectedStartTime, addNew.NewDisconnection.StartTime);
Assert.Null(addNew.NewDisconnection.EndTime);

Would this fit your needs?

My end goal is that when a unit test fails, the error tells me something like:

Message: Expected 'AddNew' but actual is 'UpdateLastEndTime`

Unfortunately, so far:

  • result.MatchAddNew(true => {}, false =>{}) won't tell what the value is on the false path.
  • result.UnwrapAddNew() talks about the wrong method called, not the wrong value returned.
  • Assert.IsTrue(isAddNew) does not tell what the actual value is.

That is why my proposal from the beginning is to Add an overload of the MatchXYZ method where the false path returns the base union type.

Anyway, I have written it like and I think its better than the UnwrapXYZ, more expressive and explicit about what is being checked:

result.MatchAddNew(
    addNew =>
    {
        Assert.Equal(expectedHardwarUnitId, addNew.NewDisconnection.HardwareUnitId);
        Assert.Equal(expectedState, addNew.NewDisconnection.State);
        Assert.Equal(expectedStartTime, addNew.NewDisconnection.StartTime);
        Assert.Null(addNew.NewDisconnection.EndTime);
    },
    () => Assert.Fail($"Expected 'AddNew' but result is '{result.GetType().Name}")
    );

I think your best option is to assert against the type using whatever assertion library you're using, then. For example:

var result = BusinessLogic.DetermineLogChanges(e, null);
var addNew = result as AddNew;

// Xunit
Assert.IsType<AddNew>(result);

// FluentAssertions
result.Should().BeOfType<AddNew>();

// More assertions against `addNew`.
// ...
}

I don't find that the testing use case justifies an update to this library due to the suitable alternatives discussed here.

All right, thanks for the tips, will go into this direction!