We left the story in the “how” retrieve the data from the profile system in our tests. Introducing the ProfileTestProvider.
The main responsibility of this class is performing in-memory operations during our tests, so that our properties can be retrieved and stored, as well as initialized, all of which will be performed without accessing any external storage.
Another responsibility we place on its shoulders is to ease assertions performed on the values of properties.
Not two bad, let’s see some code!
The Provider
public class ProfileTestProvider : ProfileProvider | |
{ | |
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection) | |
{ | |
return mergeCollections(collection, Properties); | |
} | |
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection) | |
{ | |
Properties = collection; | |
} | |
// ... bunch of uninteresting methods |
public SettingsPropertyValueCollection Properties { get; set; } | |
public void AssertPropertyValue(string propertyName, Constraint constraint) | |
{ | |
SettingsPropertyValue p = Properties[propertyName]; | |
Assert.That(p.PropertyValue, constraint); | |
} | |
public void StubValues(params KeyValuePair<string, object>[] values) | |
{ | |
Properties = new SettingsPropertyValueCollection(); | |
foreach (var pair in values) | |
{ | |
Type type = pair.Value.GetType(); | |
var settings = new SettingsProperty(pair.Key, type, null, false, defaultOf(type), | |
SettingsSerializeAs.ProviderSpecific, null, false, false); | |
var settingsValue = new SettingsPropertyValue(settings) { PropertyValue = pair.Value }; | |
Properties.Add(settingsValue); | |
} | |
} | |
private static SettingsPropertyValueCollection mergeCollections(SettingsPropertyCollection noValues, SettingsPropertyValueCollection withValues) | |
{ | |
SettingsPropertyValueCollection merged = new SettingsPropertyValueCollection(); | |
foreach (SettingsProperty property in noValues) | |
{ | |
var setting = new SettingsPropertyValue(property); | |
if (withValues != null && withValues[property.Name] != null && withValues[property.Name].PropertyValue != null) | |
{ | |
setting.PropertyValue = withValues[property.Name].PropertyValue; | |
} | |
merged.Add(setting); | |
} | |
return merged; | |
} | |
private static object defaultOf(Type t) | |
{ | |
if (!t.IsValueType) return null; | |
return Activator.CreateInstance(t); | |
} | |
} |
Not a lot of code, but still enough to deserve some explanations:
- GetPropertyValues(): Invoked when values from the profile need to be accessed, it merges the collection of value-less properties coming from configuration, with the externally-visible collection of properties that will (hopefully) contain some values coming from some sort of setup in the test
- SetPropertyValues(): Invoked when the profile is saved, will replace the externally-visible collection of properties to be examined with the one that contains the current values, allowing its inspection and assertions
- StubValues(): allows setting values to the profile properties prior to their retrieval. It will be invoked during the test “arrange” phase
- AssertPropertyValue(): allows performing an NUnit assertion to a value of the collection. It will be invoked during the “assert” phase of the test
Reading from the Profile
Now… how would class like this will make the Retrieve_ExistingEmail_InformationFromProfileAndAnotherDataSource() test pass? It’ll be just a matter of configuring the profile system for our test project, specifying our in-memory provider, as well as the set of properties to retrieve (in the case of the HybridClass only these two properties are needed):
<?xml version="1.0" encoding="utf-8" ?> | |
<configuration> | |
<system.web> | |
<profile enabled="true" defaultProvider="testProvider"> | |
<providers> | |
<clear /> | |
<add name="testProvider" type="DgonDotNet.Blog.Samples.AspNetProfiles.ProfileTestProvider, DgonDotNet.Blog.Samples.AspNetProfiles"/> | |
</providers> | |
<properties> | |
<clear/> | |
<add name="Gender" type="System.Nullable`1[System.Byte]" serializeAs="ProviderSpecific" defaultValue="[null]" /> | |
<add name="DayOfBirth" type="System.Nullable`1[System.DateTime]" serializeAs="ProviderSpecific" defaultValue="[null]"/> | |
</properties> | |
</profile> | |
</system.web> | |
</configuration> |
And, if you believe me (or better, run the actual test in the code provided) you are going to depict a big, bright green light. But coming to review the code for the newly passed test, we found nothing of interest to exercise our new provider, it was just checking the properties coming from the other data source, not from the profile. Let’s complete the test to proves that we are indeed getting the properties from the profile system:
[Test] | |
public void Retrieve_ExistingEmail_InformationFromProfileAndAnotherDataSource() | |
{ | |
string existingEmail = "me@here.com"; | |
decimal fromAnotherSource = 10m; | |
DateTime birthDay = new DateTime(1977, 3, 11); | |
byte femaleRepresentation = 1; | |
ProfileTestProvider provider = (ProfileTestProvider)ProfileManager.Provider; | |
var subject = initSubject(); | |
_another.Stub(a => a.Retrieve(existingEmail)).Return(fromAnotherSource); | |
provider.StubValues( | |
new KeyValuePair<string, object>(UserProfile.DAYOFBIRTH, birthDay), | |
new KeyValuePair<string, object>(UserProfile.GENDER, femaleRepresentation)); | |
HybridClass retrieved = subject.Retrieve(existingEmail); | |
Assert.That(retrieved.NotFromProfile, Is.EqualTo(fromAnotherSource)); | |
// another post subject would be how to handle temporal-dependant clases, but not this one | |
Assert.That(retrieved.AgeFromProfile.Value.Advent, Is.EqualTo(birthDay)); | |
Assert.That(retrieved.GenderFromProfile.Value, Is.EqualTo(AGenderEnum.Female)); | |
} |
Again, the faith of the reader will allow me to say this test is passing.
Writing to the Profile
Armed with the confidence that the service it is actually reading information from the profile, let us test that it writes back the data to the correct data-source:
[Test] | |
public void Persist_InformationToProfileAndAnotherDataSource() | |
{ | |
string email = "me@here.com"; | |
DateTime birthDay = new DateTime(1977, 3, 11), today = new DateTime(2010, 4, 30); | |
byte maleRepresentation = 0; | |
AnAgeValueObject age = new AnAgeValueObject(birthDay, today); | |
HybridClass toBePersisted = new HybridClass(email, age, AGenderEnum.Male, 15m); | |
ProfileTestProvider provider = (ProfileTestProvider)ProfileManager.Provider; | |
ServiceClass subject = initSubject(); | |
subject.Persist(toBePersisted); | |
_another.AssertWasCalled(a => a.Save(email, 15m)); | |
provider.AssertPropertyValue(UserProfile.DAYOFBIRTH, Is.EqualTo(birthDay)); | |
provider.AssertPropertyValue(UserProfile.GENDER, Is.EqualTo(maleRepresentation)); | |
} |
Take-Aways
I think we can take several things away from this brief series:
- Classes that interact with the profile system via a ProfileBase instance can be easy unit tested by stubbing out the surface of contact with the profile infrastructure
- How to include configuration into test projects
- Interactions with the profile system can be tested by the means of a provider built for testing
So, next time you feel the need of using the profile system, do not ditch it upfront because you think it is going to be difficult to test.