F#: Why those two collections are not equal?

2019-07-23 07:13发布

问题:

Was writing some units using XUnit until that at some points I bumped into something surprising:

let id = Guid.Empty
let contact = {
    Name = {
        FirstName = "Marcel"
        MiddleInitial = None
        LastName = "Patulacci"
    }
    DateOfBith = new DateTime(1850, 12, 25)
    Address = {
        Address1 = "41 av 8 Mai 1945"
        Address2 = None
        City = "Sarcelles"
        State = None
        Zip = "95200"
    }
    PhoneNumber = {
        DialOutCode = 33
        LocalNumber = "766030703"
    }
    Email = "marcel.patulacci@outlook.com"
}

[<Fact>]
let ``Open an account...``() =
    let event = Event.AccountOpened({
        AccountId = id
        Contact = contact
    })

    let a = [event]
    let b = seq { yield event }

    Assert.Equal(a, b)

System.NullReferenceException : Object reference not set to an instance of an object.

It seems surprising especially since considering that the overload used by Assert is:

public static void Equal<T>(IEnumerable<T> expected, IEnumerable<T> actual)

Which states that:

Verifies that two sequences are equivalent, using a default comparer.

Why are they considered different, and why does Assert.Equal raise a System.NullReferenceException?

[EDIT]

System.NullReferenceException : Object reference not set to an instance of an object. at Domain.Events.AccountOpenedEvent.Equals(Object obj, IEqualityComparer comp) at Domain.Events.Event.Equals(Object obj, IEqualityComparer comp)

Seems


type PersonalName = {
    FirstName: string;
    MiddleInitial: string option;
    LastName: string;
}

type Address = {
    Address1: string;
    Address2: string option ;
    City: string;
    State: string option;
    Zip: string;
}

type PhoneNumber = {
    DialOutCode : int;
    LocalNumber: string
}

type Contact = {
    Name: PersonalName;
    DateOfBith: DateTime
    Email: string;
    Address: Address;
    PhoneNumber: PhoneNumber
}

type AccountOpenedEvent = {
    AccountId: Guid
    Contact: Contact
}

type Event =
    | AccountOpened of AccountOpenedEvent

It turns out one of the fields of event was null, but not event itself.

回答1:

The problem resided in the id and contact that were defined right above the test / [<Fact>]:

let id = Guid.Empty
let contact = {
    Name = {
        FirstName = "Marcel"
        MiddleInitial = None
        LastName = "Patulacci"
    }
    DateOfBith = new DateTime(1850, 12, 25)
    Address = {
        Address1 = "41 av 8 Mai 1945"
        Address2 = None
        City = "Sarcelles"
        State = None
        Zip = "95200"
    }
    PhoneNumber = {
        DialOutCode = 33
        LocalNumber = "766030703"
    }
    Email = "marcel.patulacci@outlook.com"
}

[<Fact>]
let ``Open an account...``() =
    let event = Event.AccountOpened({
        AccountId = id
        Contact = contact
    })

    let a = [event]
    let b = seq { yield event }

    Assert.Equal(a, b)

The thing is when running the test independently the id and contact are not initialized, hence even though event was not null, contact was null (id being a Guid aka a struct it has a value anyway).

Since F# works with structural equality, if one of the field is not initialized it was enough to have a field null to make the Assert failed at some point in its implementation.

There are a few solutions / workarounds:

  1. Defining those variables directly in the unit test body.
  2. Defining methods which produce those values out of the unit test body
let getId() = Guid.Empty
let getContact() = {
    Name = {
        FirstName = "Marcel"
        MiddleInitial = None
        LastName = "Patulacci"
    }
    DateOfBith = new DateTime(1850, 12, 25)
    Address = {
        Address1 = "41 av 8 Mai 1945"
        Address2 = None
        City = "Sarcelles"
        State = None
        Zip = "95200"
    }
    PhoneNumber = {
        DialOutCode = 33
        LocalNumber = "766030703"
    }
    Email = "marcel.patulacci@outlook.com"
}

[<Fact>]
let ``Open an account...``() =
    let id = getId()
    let contact = getContact()

    let event = Event.AccountOpened({
        AccountId = id
        Contact = contact
    })

    let a = [event]
    let b = seq { yield event }

    Assert.Equal(a, b)

While those workarounds work, I am surprised that the variables declared right above the unit test function are not considered when the test is running / and are uninitialized.

It might worth to shoot another question about why this is the case. This is surprising in the sense that if a function can be defined and returning pretty much the same thing as those variables it means that let is also properly compiled, so why this is not the case with the variables?



标签: c# f# xUnit