20 March 2011

When you have a big solution with a lot of DTOs and Entities that map one to another across layers, it creates a risk of fields that are forgotten to be added to one or another, or mapped. Such errors can be very hard to spot by the test team, and they may easily end up in production.

In my previous article I described the benefits of using AutoMapper, which also include ease of unit testing. But there is a downside to it - Performance!

When it comes to choosing between performance and ease of unit testing, the choice is usually simple. Even I, in spite of how much I like it, chose to go the old fashion way, and to do the mapping methods manually.

I've been giving a lot of thought on how to be able to test the mapping methods, when AutoMapper is not used, and the conclusion I came to is.. to use AutoMapper! Don't get too confused, I decided on using the AutoMapper in the unit tests so a parallel automatic mapping is done that will verify the manual mapping.

But before I could do that, I needed a way to compare two objects that did not have Equals() method overloaded, as I could not count on it to be implemented for the tested entities, or for it to be correctly implemented. The solution was reflection.

Let's dig into it and see how it all works nicely. First the DTOs:

[csharp]
public class CustomerDto
{
public AddressDto Address { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public int? Accusitions { get; set; }
}

public class AddressDto
{
public Country Country { get; set; }
public string City { get; set; }
public string AddressLine { get; set; }
public string ZipCode { get; set; }
}

public enum CountryDto
{
USA,
UK
}
[/csharp]

The DTOs are a customer with some properties and with an address. Now for the Domain classes:

[csharp]
public class Customer
{
public Address Address { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public int? Accusitions { get; set; }
}

public class Address
{
public Country Country { get; set; }
public string City { get; set; }
public string AddressLine { get; set; }
public string ZipCode { get; set; }
}

public enum Country
{
USA,
UK
}
[/csharp]

Now that we have the required DTOs and Domain classes, let's write some mapping methods:

[csharp]
public static class Mapper
{
public static Customer ToDomain(this CustomerDto dto)
{
if (dto == null) return null;

return new Customer
{
Accusitions = dto.Accusitions,
Address = dto.Address.ToDomain(),
DateOfBirth = dto.DateOfBirth,
Name = dto.Name
};
}

public static Address ToDomain(this AddressDto dto)
{
if (dto == null) return null;

return new Address
{
AddressLine = dto.AddressLine,
City = dto.City,
Country = (Country)dto.Country,
ZipCode = dto.ZipCode
};
}
}
[/csharp]

The Mapper contains extension method for mapping, this way it's simpler and more intuitive to use them. Implementing the mapping method in the DTO class or in the Domain class is often impossible, and as a general rule bad practice.

Now that everything that we want to be tested is in place, let's see how a classical UT would look like:

[csharp]
[TestMethod]
public void Customer_Mapping_Classic_Test()
{
//create a DTO object that will be mapped to Domain
CustomerDto customerDto = new CustomerDto
{
Accusitions = 10,
DateOfBirth = new DateTime(1980, 3, 3),
Name = "Robert Black",
Address = new AddressDto
{
Country = CountryDto.UK,
AddressLine = "Some street, 27, ap. 10",
City = "London",
ZipCode = "423562"
}
};

//calling the tested method
Customer customer = customerDto.ToDomain();

//verify the expectations
Assert.AreEqual(10, customer.Accusitions);
Assert.AreEqual(new DateTime(1980, 3, 3), customer.DateOfBirth);
Assert.AreEqual("Robert Black", customer.Name);
Assert.AreEqual(Country.UK, customer.Address.Country);
Assert.AreEqual("Some street, 27, ap. 10", customer.Address.AddressLine);
Assert.AreEqual("London", customer.Address.City);
Assert.AreEqual("423562", customer.Address.ZipCode);
}
[/csharp]

As you can see it is straight forward. But what does it really test? Only that the end values are those expected. It won't make any assumptions for new fields that will be added to the DTO or Domain classes unless the programmer remembers to add them to the UT. Another downside is, if you have a big complex object, it takes a lot of lines to write the test, and more than that, it is error prone.

But I wanted a UT that will test my mappings at all times, and under any circumstances. If a field is added to the Domain class and corresponding field isn't added and mapped to DTO class I want my tests to catch that, and fail until the programmer explicitly ignores that field as being relevant only to the BL Layer.

Let's see the test and then discuss it:

[csharp]
[TestMethod]
public void Customer_Mapping_Test()
{
AutoMapper.Mapper.CreateMap<CustomerDto, Customer>();
AutoMapper.Mapper.CreateMap<Customer, CustomerDto>();
AutoMapper.Mapper.CreateMap<AddressDto, Address>();
AutoMapper.Mapper.CreateMap<Address, AddressDto>();
AutoMapper.Mapper.AssertConfigurationIsValid();

//create a DTO object that will be mapped to Domain
CustomerDto customerDto = new CustomerDto
{
Accusitions = 10,
DateOfBirth = new DateTime(1980, 3, 3),
Name = "Robert Black",
Address = new AddressDto
{
Country = CountryDto.UK,
AddressLine = "Some street, 27, ap. 10",
City = "London",
ZipCode = "423562"
}
};

Customer manualMapping = customerDto.ToDomain();
Customer autoMapping = AutoMapper.Mapper.Map<CustomerDto, Customer>(customerDto);

bool testResult = ReflectionComparer.Equal(manualMapping, autoMapping);
Assert.IsTrue(testResult);
}
[/csharp]

So what is different? First of all, there is the AutoMapper involved. The 2 way mapping has been defined on the lines 4 to 7, and then the mappings have been validated on line 8. If the mappings won't be 1 to 1 the AssertConfigurationIsValid will throw an Exception. In my previous article I've described how to use this mappings. You can set rules to ignore fields, or to map one field to another with a different name, but for that you should read the referred link from the previous article.

The DTO object initialization is the same as in the classical test. We create a Customer object using our mapping method ToDomain(). Next the magic starts, anothe Customer object is created using the automatic mapping. The mapping is done using rules set up on lines 4-7. In our test we supposed the mapping is one to one so no rules were necessary.

The last, and most important part is the ReflectionComparer.Equal() method. Using reflection it compares the two objects to have same values. Here is the code:

[csharp]
public class ReflectionComparer
{
/// <summary>
/// Compares two objects by reflection and decides if they are equal as value or not.
/// </summary>
/// <param name="source">The source object to compare.</param>
/// <param name="target">The target object to be compared against.</param>
/// <returns>true if the objects are same as value, false otherwise.</returns>
public static bool Equal(object source, object target)
{
//check for null and null combinations
if (source == null && target == null) return true;
if (source == null || target == null) return false;

//after this point source and target are sure to be instantiated

Type sourceType = source.GetType();
Type targetType = target.GetType();
if (!sourceType.Equals(targetType)) return false;

//get all the properties and compare them
PropertyInfo[] properties = sourceType
.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo propertyInfo in properties)
{
//skip setter only properties
if (!propertyInfo.CanRead) continue;

//check if it is a index parameter and skip if so
if (propertyInfo.GetIndexParameters().Count() != 0) continue;

//read the values of the properties for source and target
Object sourceValue = propertyInfo.GetValue(source, null);
Object targetValue = propertyInfo.GetValue(target, null);

//first check for nulls
//if both nulls skip to the next property
if (sourceValue == null && targetValue == null) continue;
//if one is null and the other is not then the objects
//are not same as value
if (sourceValue == null || targetValue == null) return false;

if (propertyInfo.PropertyType.IsPrimitive ||
propertyInfo.PropertyType.IsEnum ||
propertyInfo.PropertyType == typeof(string))
{
//all primitive types, enums and strings can be
//safely compared using Equals()
if (!sourceValue.Equals(targetValue)) return false;
}
else if (propertyInfo.PropertyType.IsValueType)
{
//unless you use structs it's safe to suppose it's a standard type
//and can be compared with Equals()
if (!sourceValue.Equals(targetValue)) return false;
}
else if (sourceValue is System.Collections.IEnumerable)
{
//special treatment is required, unless it is used
//no need to support it
throw new NotImplementedException();
}
else
{
//finally we can do recursive comparison
if (!Equal(sourceValue, targetValue)) return false;
}
}

//if we got here it's safe to assume the values are same
return true;
}
}
[/csharp]

I wrote this method to suit my needs, and must warn you, it is not ready to compare any classes. The limitations of which I am aware of (can't guarantee it's bug free): can't compare properties that are List, Dictionary and so on. For that I added the condition for IEnumerable and an exception throwin NotImplementedException(), so in case there happens to be such an case no strange results will be returned. Feel free to implement it if you need it in your code, and don't forget to drop me a message with the solution :) The other limitation, all Structures must have the Equals() method overridden. Actually that's more of a good practice than a limitation.

As a side note, this Equal() method can be used for unit testing the overridden Equals() methods.

At this point all the insights of the UT are explained, but not much as general. This unit test will check that the DTO and Domain classes are same as structure, and that the mapping method maps all properties. Whenever a new set of properties will be added to both classes, and the mapping will be updated for them, the unit test will be already validating that. If the programmer adds the properties but forgets to update the mapping, the test will fail. If the programmer adds a property only in one class, the test will fail. If the Properties are named differently in the DTO and Domain the test will fail. For the last two, special conditions might apply, and the programmer might not want all properties from Domain being mapped to the DTO. And naming of the properties can be different as desired by some conditions.

Because I defined mapping method only from DTO to Domain, I'm going to add a new field to the CustomerDTO that I do not want to be mapped into the Domain object, check that the test fails, and then update the test to explicitly ignore this new field.

[csharp]
public class CustomerDto
{
public AddressDto Address { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public int? Accusitions { get; set; }
public string SessionCode { get; set; }
}
[/csharp]

The new field is SessionCode. If I rerun the test, the test will fail with the exception:

Test method MappingTests.UnitTest.Customer_Mapping_Test threw exception:
AutoMapper.AutoMapperConfigurationException: The following 1 properties on MappingTests.CustomerDto are not mapped:
SessionCode
Add a custom mapping expression, ignore, or rename the property on MappingTests.Customer.

Now to update the test:

[csharp]
[TestMethod]
public void Customer_Mapping_Test()
{
AutoMapper.Mapper.CreateMap<CustomerDto, Customer>();
AutoMapper.Mapper.CreateMap<Customer, CustomerDto>()
.ForMember(x => x.SessionCode, y => y.Ignore());
AutoMapper.Mapper.CreateMap<AddressDto, Address>();
AutoMapper.Mapper.CreateMap<Address, AddressDto>();
AutoMapper.Mapper.AssertConfigurationIsValid();

//create a DTO object that will be mapped to Domain
CustomerDto customerDto = new CustomerDto
{
Accusitions = 10,
DateOfBirth = new DateTime(1980, 3, 3),
Name = "Robert Black",
Address = new AddressDto
{
Country = CountryDto.UK,
AddressLine = "Some street, 27, ap. 10",
City = "London",
ZipCode = "423562"
}
};

Customer manualMapping = customerDto.ToDomain();
Customer autoMapping = AutoMapper.Mapper.Map<CustomerDto, Customer>(customerDto);

bool testResult = ReflectionComparer.Equal(manualMapping, autoMapping);
Assert.IsTrue(testResult);
}
[/csharp]

The added line is .ForMember(x => x.SessionCode, y => y.Ignore());. That tells AutoMapper to ignore this field when mapping from a Domain object to a DTO, but our mapping is the other way arround?.. Well, wehave to validate AutoMapper rules both ways to spot new properties on DTO or Domain classes.

Next special case is when the names are different. I'll rename the Country property from the Address Domain class to HomeCountry:

[csharp]
public class Address
{
public Country HomeCountry { get; set; }
public string City { get; set; }
public string AddressLine { get; set; }
public string ZipCode { get; set; }
}
[/csharp]

If I run the UTs now, I will get one UT failed, the one with AutoMapper. Unfortunately this can't be simplified in any way, so I'll have to go and update the mapping rules for AUtoMapper:

[csharp]
[TestMethod]
public void Customer_Mapping_Test()
{
AutoMapper.Mapper.CreateMap<CustomerDto, Customer>();
AutoMapper.Mapper.CreateMap<Customer, CustomerDto>()
.ForMember(x => x.SessionCode, y => y.Ignore());
AutoMapper.Mapper.CreateMap<AddressDto, Address>()
.ForMember(x => x.HomeCountry, y => y.MapFrom(z => z.Country));
AutoMapper.Mapper.CreateMap<Address, AddressDto>()
.ForMember(x => x.Country, y => y.MapFrom(z => z.HomeCountry));
AutoMapper.Mapper.AssertConfigurationIsValid();

//create a DTO object that will be mapped to Domain
CustomerDto customerDto = new CustomerDto
{
Accusitions = 10,
DateOfBirth = new DateTime(1980, 3, 3),
Name = "Robert Black",
Address = new AddressDto
{
Country = CountryDto.UK,
AddressLine = "Some street, 27, ap. 10",
City = "London",
ZipCode = "423562"
}
};

Customer manualMapping = customerDto.ToDomain();
Customer autoMapping = AutoMapper.Mapper.Map<CustomerDto, Customer>(customerDto);

bool testResult = ReflectionComparer.Equal(manualMapping, autoMapping);
Assert.IsTrue(testResult);
}
[/csharp]

Horray, the tests are green! Now I used ForMember() method again, but instead of Ignore(), I mapped the fields using MapFrom(). More complex rules for mapping can be defined, but this are not part of this article.

To conclude it all, unit test careful your mapping methods, and you might end up doing a lot less debugging in the long run.



blog comments powered by Disqus