views:

367

answers:

1

I'm brand new to unit testing and mocking and still wet behind the ears. I'm using the Moq framework and I need to mock a collection such that it yields a single member with a value I supply.

The collection class in question is a System.Configuration.SettingsPropertyCollection, which contains SettingsProperty objects.

In turn, the SettingsProperty has an Attributes property that returns a SettingsAttributeDictionary.

I need my collection to yield a single SettingsProperty, which has a single custom attribute (derived from System.Attribute) in its Attributes/SettingsAttributeDictionary.

I'm really struggling to get my head around this, but so far to no avail. I've tried sticking out my tongue and pulling funny faces at it, but nothing works.

Here's the code I've tried so far, an exception is thrown at the point commented in the code and so of course the test always fails.

    [TestMethod]
    public void GetPropertySettings_Should_Return_Default_Values_When_Device_Not_Registered()
        {
        const string deviceName = "My.UnitTest";
        const string deviceType = "Switch";
        var deviceId = String.Format("{0}.{1}", deviceName, deviceType);
        Mock<IProfile> mockProfile = new Mock<IProfile>();
        Mock<SettingsContext> mockSettingsContext = new Mock<SettingsContext>();
        // Construct a SettingsPropertyCollection populated with a single property.
        // The single property will have a fixed name and default value, and will also have a single
        // attribute, giving teh ASCOM DeviceId.
        var deviceAttribute = new ASCOM.DeviceIdAttribute(deviceId);
        var attributes = new SettingsAttributeDictionary();
        attributes.Add(typeof(DeviceIdAttribute), deviceAttribute);
        var settingsProperty = new SettingsProperty(SettingName, typeof(string), null, false, SettingDefaultValue, SettingsSerializeAs.String, attributes, true, true);
        var propertyCollection = new SettingsPropertyCollection();
        propertyCollection.Add(settingsProperty);

        // Now comes the interesting part where we call our IProfile - this is where we really need Moq.
        // Expectations:
        //  - mockProfile must have it's DeviceType set.
        //  - mockProfile's device type (captured in setDeviceType) must match deviceType.
        //  - The returned SettingsPropertyValueCollection must not be empty.
        //  - The returned SettingsPropertyValueCollection must have exactly one entry.
        //  - The entry must match the value of SettingDefaultValue. 

        // Expectation: IProfile must have its DeviceType set.  We capture the value into setDeviceType.
        var setDeviceType = String.Empty;
        mockProfile.SetupSet(x => x.DeviceType).Callback(y => setDeviceType = y);

        // Finally, it is time to call the method we want to test
        var settingsProvider = new SettingsProvider(mockProfile.Object);

        // THE NEXT LINE THROWS AN EXCEPTION
        // IF I TRY TO STEP INTO IT, IT NEVER RETURNS AND THE TEST RUN JUST ENDS.
        var result = settingsProvider.GetPropertyValues(mockSettingsContext.Object, propertyCollection);

        // Now lets verify that everything went as expected
        // First, let's test that the parsing of DeviceId was as expected: IProvider.DeviceType was set to the expected value
        Assert.AreEqual(deviceType, setDeviceType);
        // Then let's test that the methods of IProvider that we mocked were called
        mockProfile.VerifyAll();

        // With this done, let's turn to the output of the method
        // Firstly, we test that the resulting collection contains exactly one item of the type SettingsPropertyValue
        Assert.IsTrue(result.Count > 0);
        Assert.AreEqual(1, result.Count);
        Assert.IsTrue(result.OfType<SettingsPropertyValue>().Count() > 0);

        // Then let's inspect the contained SettingsProviderValue further
        var settingsPropertyValue = result.OfType<SettingsPropertyValue>().First();

        // First IsDirty flag must never be set
        Assert.IsFalse(settingsPropertyValue.IsDirty);

        // The PropertyValue must be the default value we passed in
        Assert.AreEqual(SettingDefaultValue, settingsPropertyValue.PropertyValue);
        }

The exception that gets thrown (as reported by the test runner) is:

Test method ASCOM.Platform.Test.SettingsProviderTest.GetPropertySettings_Should_Return_Default_Values_When_Device_Not_Registered threw exception: System.ArgumentException: The type System.Configuration.SettingsContext implements ISerializable, but failed to provide a deserialization constructor.

+1  A: 

I believe you need to set up the Attributes property of your mock SettingsProperty to return your mock SettingsAttributeDictionary and then set up the indexer of your mock SettingsAttributeDictionary to return your desired value, e.g.

mockItem.SetupGet(x => x.Attributes).Returns(mockAttributes);

mockAttributes.SetupGet(x => x[It.IsAny<System.Type>()])
              .Returns(deviceAttribute);

Edit

The exception is being thrown in the Castle DynamicProxy class which is used by Moq. I believe this is to do with how Moq mocks objects which are serializable. If castle can't find a non-public constructor on a serializable object with a signature of (SerializationInfo, StreamingContext) then it throws this exception. What you could do, is change the GetPropertyValues method of your custom SettingsProvider so that it accepts a Hashtable instead of a SettingsContext, and provide a mock Hashtable to the method call instead of a mock SettingsContext. Hashtable has the required constructor so perhaps this will work. There is no real advantage to insisting on a parameter of type SettingsContext rather than Hashtable since SettingsContext is only trivially derives from Hashtable, i.e. it adds no members.

AdamRalph
Thanks for the suggestion. I juggled my code around per you suggestion, but no dice.I still get an exception (and a failed test) on the first of the two lines.The exception is: System.ArgumentException: The type System.Configuration.SettingsAttributeDictionary implements ISerializable, but failed to provide a deserialization constructor.Hmmm...
Tim Long
I've edited in light of the exception details
AdamRalph
OK! Your suggestion didn't work directly, but you did lead me to the answer. I couldn;t change the method signatures because they are marked abstract in the base class and no matter what I tried, I got a compile error. So, I derived a new class from SettingsContext and added the serialization constructor, and that works. It smells a bit, having to fudge the 'real' code to make the unit tests pass, and that has jaundiced my first experience of unit testing and mocking, but at least I can move forward now. So, you get the bounty!
Tim Long
thank you! try not to let this put you off unit testing/mocking. I've been doing extensive work with both xUnit.net and Moq for a while now and this is the first time I've seen this problem. It's just a little unfortunate that you stumbled across something so tricky so early on! ;-)
AdamRalph