Click here to Skip to main content
15,867,453 members
Articles / DevOps / Unit Testing

Generic Mapper Written in TDD

Rate me:
Please Sign up or sign in to vote.
4.77/5 (13 votes)
9 Feb 2016CPOL5 min read 18.5K   18   9
Write your own simple generic mapper with TDD
This article shows how to easily write own simple generic mapper with TDD. This is a great exercise to practice TDD that results in fully-working and simple generic mapper that can be used in production projects.

Introduction

The article shows how to write your own mapper in a step by step manner.
Using tests-first approach makes it easier to understand what this mapper actually does and how to implement complex features from scratch.

Background

What are mappers and why use them?

Mappers are used to rewrite data from one object to another. Often, these objects are the same Business object, they just belong to different application layers.

In simple and small applications, mappers are often not needed. Rewriting data from one object to another is done manually, as usually these objects are completely different. For example:

C#
var email = new Email
{
    From = user.Name,
    Topic = topicTxt.Text,
    Body = bodyTxt.Text,
    Date = DateTime.Now
};

But in larger applications with more layers of abstraction, there are many different classes that represent the same business entity. This can be seen when there is a combination of Data Access Layer (entities in EntityFramework), Data Transaction Objects (objects passed by the WCF) or ViewModels (objects displayed in the View layer, for example, in the MVC applications). Application divided in these layers can look like this:

C#
var customerDto = new CustomerDto
{
    Id = customerEntity.Id,
    Name = customerEntity.Name,
    Surname = customerEntity.Surname
};

var customerViewModel = new CustomerViewModel
{
    Id = customerDto.Id,
    Name = customerDto.Name,
    Surname = customerDto.Surname
};

Of course, with help come constructors that accept another object or factory methods. But still there will be some initialization in the code.

A mapper could do this "magically" :)

C#
var customerDto = mapper.Map<CustomerDto>(customerEntity);

var customerViewModel = mapper.Map<CustomerViewModel>(customerDto);

Let this mindless and stupid work be done automatically so we could save our time for real problems. ;)

TDD stands for Test Driven Development. The idea is to write the Unit Test firstly, then to write implementation that would satisfy the test.

Writing tests is a big advantage. Makes the code resistant to modifications. It also saves time in testing the application, especially when there are many paths to check or when testing the application is time-consuming.

In my opinion, a TDD has another great benefits.
Sometimes, the result of the operation is simple, but the implementation is vague and at first sight, it is very difficult to write the code.
Another benefit, especially when it comes to API is that these Unit Tests are basic usage of the API. This means that when writing the tests, it is already known how this API would be used. I think that there are many tools, that makes a lot of great stuff, but they are really hard and unclear to use. In other words, if you're not a user of the API, you don't care how somebody would use it. And it is wrong. :)

Using the Code

For those who are not familiar with TDD or even with Unit Tests, our journey will start by creating a new project.

In Visual Studio, select File -> New...
In the New Project window, search for Unit Test Project (can be found in Templates -> Visual C# -> Test).
Type the name for the project (e.g., SandboxTests) and create the project.

Now create a new Unit-Tests class.
Right click on the project, select Add -> Unit Test...
Then change the name of the file to GenericMapperTests.cs.

Visual Studio automatically prepared all references necessary for writing Unit Tests. So...

Let's write our first Unit Test.

C#
[TestClass]
public class GenericMapperTests
{
    [TestMethod]
    public void ShouldMapPropertiesFromOneObjectToAnother()
    {
        // Given

        // When

        // Then
    }
}

What we would like to do is create and assign one object. Then create the same object and rewrite values from the first one to the second.

The complete test should look like this:

C#
[TestMethod]
public void ShouldMapPropertiesFromOneObjectToAnother()
{
    // Given
    var customer = new Customer
    {
        Id = 1,
        Name = "Miłosz",
        Surname = "Wieczorek"
    };
    var newCustomer = new Customer();
    var mapper = new GenericMapper();

    // When
    mapper.Map(customer, newCustomer);

    // Then
    Assert.AreEqual(customer.Id, newCustomer.Id, "Id");
    Assert.AreEqual(customer.Name, newCustomer.Name, "Name");
    Assert.AreEqual(customer.Surname, newCustomer.Surname, "Surname");
}

Writing this test, we have generated some basic object Customer and our destination - GenericMapper class.

C#
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}

public class GenericMapper
{
    public void Map(Customer customer, Customer newCustomer)
    {
    }
}

As the title of this article says, it should be a Generic Mapper. So let's fix the method signature.
By the way, let's make some first implementation that satisfies the test.

C#
public class GenericMapper
    {
        public void Map<T>(T from, T to)
        {
            var type = typeof(T);
            var properties = type.GetProperties();
            foreach (var property in properties)
            {
                property.SetValue(to, property.GetValue(from));
            }
        }
    }

There is no rocket science, but it is a basic implementation that satisfies our test.

Now the Mapper maps properties from one object to another, but these objects need to be the same type. Let's improve a little bit our Mapper so it could map values from one type to another, as it is a real objective of the Mapper.

As we know what the Mapper should do, writing second test is pretty obvious.

C#
[TestMethod]
public void ShouldMapPropertiesFromOneObjectToAnotherWithDifferentTypes()
{
    // Given
    var customer = new Customer
    {
        Id = 1,
        Name = "Miłosz",
        Surname = "Wieczorek"
    };
    var newCustomer = new CustomerDto();
    var mapper = new GenericMapper();

    // When
    mapper.Map(customer, newCustomer);

    // Then
    Assert.AreEqual(customer.Id, newCustomer.Id, "Id");
    Assert.AreEqual(customer.Name, newCustomer.Name, "Name");
    Assert.AreEqual(customer.Surname, newCustomer.Surname, "Surname");
}

By the way, we have created our new object, a CustomerDto that looks entirely the same as Customer.

C#
public class CustomerDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}

Unfortunately, these changes generate compilation error as the Map method in the Mapper doesn't allow to pass two different types.

Let's improve the Map method implementation so our two tests would pass.

C#
public void Map<TFrom, TResult>(TFrom from, TResult to)
{
    var typeFrom = typeof(TFrom);
    var typeTo = typeof(TResult);
    var propertiesFrom = typeFrom.GetProperties();
    var propertiesTo = typeTo.GetProperties();

    foreach (var propFrom in propertiesFrom)
    {
        foreach (var propTo in propertiesTo)
        {
            if (propTo.Name == propFrom.Name &&
                propTo.PropertyType == propFrom.PropertyType)
            {
                propTo.SetValue(to, propFrom.GetValue(from));
            }
        }
    }
}

Now run tests and... Yea our two tests are green. :)
But this code is not so pretty... Iterating through two nested collections... It doesn't look good :) Let's do some refactoring and use of LINQ.

C#
public void Map<TFrom, TResult>(TFrom from, TResult to)
{
    var typeFrom = typeof(TFrom);
    var typeTo = typeof(TResult);
    var properties = typeFrom.GetProperties()
        .Join(typeTo.GetProperties(), f => f.Name, t => t.Name, (f, t) => new
        {
            propFrom = f,
            propTo = t
        });

    foreach (var prop in properties.Where
            (p => p.propFrom.PropertyType == p.propTo.PropertyType))
    {
        prop.propTo.SetValue(to, prop.propFrom.GetValue(from));
    }
}

Now that's better.

Thanks to Unit Tests, we can easily check if our refactoring doesn't break the Map method.

Tests are green, so let's improve the Mapper. :)

Basic Mapper implementation is working fine, now let's think what features should our Mapper have.
I would like my Mapper to not write to read-only fields. I would also like to achieve the same effect in the opposite situation: a value should not be rewritten from the write-only field.

Let's prepare the test.

C#
[TestMethod]
public void ShouldNotMapReadonlyAndWriteOnlyFields()
{
    // Given
    var customer = new Customer
    {
        Id = 1,
        DateOfBirth = new DateTime(1990, 01, 01),
        Age = 26,
        UpdatedBy = 15
    };

    var customerDto = new CustomerDto();
    var mapper = new GenericMapper();

    // When
    mapper.Map(customer, customerDto);

    // Then
    Assert.AreEqual(customer.DateOfBirth, customerDto.DateOfBirth, "Date of birth");
    Assert.AreNotEqual(customer.Age, customerDto.Age, "Age");
    Assert.AreEqual(default(int), customerDto.UpdatedBy, "UpdatedBy");
    Assert.AreNotEqual(customer.GetUpdatedBy(), customerDto.UpdatedBy, "Age");
}

Here are updated objects of Customer and CustomerDto.

C#
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }

    public DateTime DateOfBirth { get; set; }
    public int Age { get; set; }

    public int UpdatedBy
    {
        private get;
        set;
    }

    public int GetUpdatedBy()
    {
        return UpdatedBy;
    }
}

public class CustomerDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }

    public DateTime DateOfBirth { get; set; }

    public int Age
    {
        get;
        private set;
    }

    public int UpdatedBy { get; set; }
}

And now let's make the test green. :)

C#
public void Map<TFrom, TResult>(TFrom from, TResult to)
{
    var typeFrom = typeof(TFrom);
    var typeTo = typeof(TResult);
    var properties = typeFrom.GetProperties()
        .Join(typeTo.GetProperties(), f => f.Name, t => t.Name, (f, t) => new
        {
            propFrom = f,
            propTo = t
        });

    foreach (var prop in properties.Where
            (p => p.propFrom.PropertyType == p.propTo.PropertyType &&
             p.propFrom.CanRead && p.propTo.CanWrite &&
             p.propFrom.GetMethod.IsPublic && p.propTo.SetMethod.IsPublic))
    {
        prop.propTo.SetValue(to, prop.propFrom.GetValue(from));
    }
}

The test is green, but the code looks ugly... A little refactor is necessary :)

C#
public class GenericMapper
{
    public void Map<TFrom, TResult>(TFrom from, TResult to)
    {
        var typeFrom = typeof(TFrom);
        var typeTo = typeof(TResult);
        var properties = typeFrom.GetProperties()
            .Join(typeTo.GetProperties(), f => f.Name, t => t.Name, (f, t) => new
            {
                propFrom = f,
                propTo = t
            });

        foreach (var prop in properties.Where
                (p => CanRewriteValue(p.propFrom, p.propTo)))
        {
            prop.propTo.SetValue(to, prop.propFrom.GetValue(from));
        }
    }

    private bool CanRewriteValue(PropertyInfo propFrom, PropertyInfo propTo)
    {
        return propFrom.PropertyType == propTo.PropertyType &&
            propFrom.CanRead &&
            propTo.CanWrite &&
            propFrom.GetMethod.IsPublic &&
            propTo.SetMethod.IsPublic;
    }
}

Much better. :)

OK, so we have nice and working Generic Mapper. But as we can see in our Unit Tests, this Mapper is very inconvenient to use.

First of all, I would like to make it static. This would affect existing tests, but it would be a small change.

Secondly, I would like my Mapper to create a new instance of object with already mapped properties. For this, a new test is necessary.

I think these would be great features that would make our Mapper more useful.

New test with creation of an object.

C#
[TestMethod]
public void ShouldCreateNewObjectWithAlreadyMappedProperties()
{
    // Given
    var customer = new Customer
    {
        Id = 4,
        Name = "John",
        Surname = "Doe"
    };

    // When
    var newCustomer = GenericMapper.Create<Customer, CustomerDto>(customer);

    // Then
    Assert.AreEqual(customer.Id, newCustomer.Id, "Id");
    Assert.AreEqual(customer.Name, newCustomer.Name, "Name");
    Assert.AreEqual(customer.Surname, newCustomer.Surname, "Surname");
}

And implementation. Pretty simple, but very useful.

C#
public static TResult Create<TFrom,
       TResult>(TFrom source) where TResult : class, new()
{
    var result = new TResult();
    Map(source, result);
    return result;
}

That's it!

Simple Generic Mapper with a few Unit Tests. Can be easily moved to another project as we are aware of what it does, how it behaves and so on.

The Generic Mapper can be extended with many functionalities. All that is needed is to write a test, then implementation that satisfies the test. After all, some refactor maybe. :)

History

  • 9th February, 2016: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Poland Poland
Hello from Poland Wink | ;)

I have been a .NET Developer since 2012. Since then I have had an opportunity to work with many projects written in technologies such as WinForms, WPF, ASP.NET MVC and ASP.NET WebAPI.
Ocasionally I did some projects in my spare time (for fun, on the studies project or as a remote part-time job).
But first of all I love coding and the possibility to increase my programming skills.

For few years I have been into client-side coding in JavaScript or TypeScript with different frameworks. I try to be up to date with latest trendings but actually the client side world is moving so fast I barerly can keep up with it Smile | :)

Cheers,
Miłosz

Comments and Discussions

 
Suggestionperformance? Pin
Irina Pykhova6-Mar-16 2:05
professionalIrina Pykhova6-Mar-16 2:05 
QuestionMap Method for Beginner Pin
Anjum.Rizwi10-Feb-16 18:53
professionalAnjum.Rizwi10-Feb-16 18:53 
SuggestionAn issue about testing - compare against absolute Pin
John Brett10-Feb-16 6:09
John Brett10-Feb-16 6:09 
AnswerRe: An issue about testing - compare against absolute Pin
Manfredzik11-Feb-16 0:29
Manfredzik11-Feb-16 0:29 
GeneralRe: An issue about testing - compare against absolute Pin
John Brett11-Feb-16 1:23
John Brett11-Feb-16 1:23 
PraiseRe: An issue about testing - compare against absolute Pin
GregoryPres1-Jan-18 2:05
GregoryPres1-Jan-18 2:05 
QuestionAn Alternative Map Method Pin
George Swan9-Feb-16 23:24
mveGeorge Swan9-Feb-16 23:24 
AnswerRe: An Alternative Map Method Pin
Manfredzik11-Feb-16 2:15
Manfredzik11-Feb-16 2:15 
GeneralRe: An Alternative Map Method Pin
George Swan11-Feb-16 3:02
mveGeorge Swan11-Feb-16 3:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.