Click here to Skip to main content
14,972,394 members
Articles / DevOps / Unit Testing
Article
Posted 12 May 2016

Stats

16.2K views
40 downloads
6 bookmarked

Testing Deep Cloning with NUnit

Rate me:
Please Sign up or sign in to vote.
4.11/5 (4 votes)
13 May 2016CPOL8 min read
Writing unit tests for a deep clone method

Introduction

In my line of work, it is fairly common to come across .NET classes that do nothing but hold information. These classes often implement their own versions of the Object.Equals and therefore Object.GetHashCode. They also occasionally need to be cloned. For simple objects that only hold onto value type objects, using Object.MemberwiseClone works just fine (making the class as struct also gets the job done). When dealing with more complex objects that hold onto other reference types, Object.MemberwiseClone is not suitable for performing deep clones. Instead, a custom deep clone method must be written. In order to ensure that a deep clone is implemented correctly, a number of unit tests can be carefully constructed to ensure that a true deep clone is performed.

Background

This article is written with the assumption that the reader knows the basics of unit testing with NUnit. Some of the examples contain snippets of NSubstitute calls as well. It is important for the reader to know the difference between shallow cloning and deep cloning as well. For more information about the difference between the two concepts, I highly recommend reading this article.

The Code Under Test

IDeepCloneable

It is recommended that programmers avoid the ICloneable interface defined in the .NET Framework (see this article). The interface is vague because code using an instance of ICloneable has no way of knowing if clone made will be a deep clone or a shallow clone. Instead of relying on ICloneable, a new interface, IDeepCloneable will be defined.

C#
public interface IDeepCloneable<T> where T: class
{
    T DeepClone();
}

The interface is implemented in terms of a generic class. It is restricted to classes because it makes little sense to deep clone a struct. Every time a struct is assigned to a new variable, it is effectively shallow cloned (passed by value). A struct that holds onto a reference could be deep cloned, but I consider storing reference types in a struct to be bad practice.

There are two classes that will be considered in this article: Cat and CatOwner. Both implement the IDeepCloneable<T> interface.

CatOwner

C#
public class CatOwner : IDeepCloneable<CatOwner>

Starting with the CatOwner object, CatOwner has a Name and Age property. Name is a reference type object (a string), and Age is a value type (an integer). There is also a constructor used for setting up a CatOwner object.

C#
public virtual string Name
{
    get;
    private set;
}

public virtual int Age
{
    get;
    private set;
}
C#
public CatOwner(string name, int number)
{
    if (string.IsNullOrEmpty(name))
        throw new ArgumentNullException("name");

    this.Name = name;
    this.Age = number;
}

An important note about the CatOwner is that it overrides both the Equals and GetHashCode methods from System.Object. This has implications later when the code is being tested.

C#
public override bool Equals(object obj)
{
    if (obj == null || !(obj is CatOwner))
        return false;

    return this.Equals(obj as CatOwner);
}

protected virtual bool Equals(CatOwner catOwner)
{
    if (!(catOwner.Age == this.Age) || !this.Name.Equals(catOwner.Name))
        return false;

    return true;
}

public override int GetHashCode()
{
    return Age.GetHashCode() ^ Name.GetHashCode();
}

Here, DeepClone is implemented using a copy constructor. Notice that String.Copy is used to copy the Name property. This ensures that the deep clone receives the value of the original object's name, not the reference to the original object's name. 

C#
protected CatOwner(CatOwner catOwner)
{
    if (catOwner == null)
        throw new ArgumentNullException("catOwner");

    this.Name = String.Copy(catOwner.Name);
    this.Age = catOwner.Age;
}

public virtual CatOwner DeepClone()
{
    return new CatOwner(this);
}

I've chosen to avoid using the serialization technique for cloning because this method is faster. The tradeoff that comes with this decision is that any classes that inherit from CatOwner and Cat are responsible for ensuring that any added references types are properly cloned when using the DeepClone method. The tests below can still work if serialization is used to perform a deep clone, however, fake objects created with NSubstitute cannot be serialized. Some of the tests would need to be adjusted to account for this.

Cat

C#
public class Cat : IDeepCloneable<Cat>

The next class to consider is the Cat class. It is similar to CatOwner, except that it has a CatOwner property.

Like the CatOwner, the Cat class also overrides Equals and GetHashCode in a similar way.

C#
public override bool Equals(object obj)
{
    if (obj == null || !(obj is Cat))
        return false;

    return this.Equals(obj as Cat);
}

protected virtual bool Equals(Cat cat)
{
    if (!(cat.Age == this.Age) || !this.Name.Equals(cat.Name) || !this.CatOwner.Equals(cat.CatOwner))
        return false;

    return true;
}

public override int GetHashCode()
{
    return CatOwner.GetHashCode() ^ Age.GetHashCode() ^ Name.GetHashCode();
}

The DeepClone method is implemented with a copy constructor as well.

C#
protected Cat(Cat cat)
{
    if (cat == null)
        throw new ArgumentNullException("cat");

    this.CatOwner = cat.CatOwner.DeepClone();
    this.Name = String.Copy(cat.Name);
    this.Age = cat.Age;
}

public virtual Cat DeepClone()
{
    return new Cat(this);
}

It is very similar to the CatOwner class's DeepClone method. Notice that it takes advantage of the CatOwner object's DeepClone method.

The Test Code

The tests are implemented using NUnit and NSubstitute. An abstract test class is defined called IDeepCloneableTests.

C#
public abstract class IDeepCloneableTests<T> where T : class, IDeepCloneable<T>

This class implements an abstract method called GetCloneableObject that will be implemented by any test fixtures for concrete implementations of IDeepCloneable.

C#
protected abstract T GetCloneableObject();

To ensure that DeepClone is implemented properly, a number of tests must be defined. The first test is to ensure the reference returned from the DeepClone method does not point to the original object that was cloned. If the references of the original and the clone are pointing to the same region of memory, then we know that DeepClone was not implemented properly.

Normally, if the Equals method were not overridden then a simple call to Assert.AreNotEqual(original, clone) would do the trick. This would default to checking if the two references are pointing to the same address in memory. Since the Cat and CatOwner implementation are able to break this behavior, the Object.ReferenceEqual(Object, Object) method should be used instead. This is a good practice, even when the objects have not overridden Equals and GetHashCode because the implementation may end up changing for one or more of the concrete classes being tested.

C#
[Test]
public void DeepClone_ClonedObject_HasDifferntMemoryAddressThanOriginal()
{
    T objectUnderTest = this.GetCloneableObject();
    T clonedObject = objectUnderTest.DeepClone();

    bool referenceEquals = Object.ReferenceEquals(objectUnderTest, clonedObject);

    Assert.False(referenceEquals);
}

To see how this test will work on a concrete implementation, take a gander at the CatOwnerTests class.

C#
[TestFixture]
public class CatOwnerTests : IDeepCloneableTests<CatOwner>
{
    protected override CatOwner GetCloneableObject()
    {
        return new CatOwner("Danny", 23);
    }
}

When this test is run, you will see that it passes. It is also important to see it break. The most illustrative way is to adjust the CatOwner.DeepClone method like so. Here the test fails because the Clone is returning a reference to the same object in memory.

C#
public virtual CatOwner DeepClone()
{
    //return new CatOwner(this);
    return this;
}

The same test can be written in the Cat class.

C#
protected override Cat GetCloneableObject()
{
    // Normally I would stub this out with NSubstitute
    // But NSubstitute fake objects cannot be serialized.
    CatOwner stubCatOwner = new CatOwner("Danny", 23);

    return new Cat(stubCatOwner, "Dr. Piddles", 4);
}

This test also passes and will break when you change the CatOwner DeepClone method to return this instead of a new, cloned Cat object. One important thing to notice is that the broken DeepClone method in the CatOwner class only breaks the test for the CatOwner class, and not the Cat class (even though it is supposed to be a deep clone). This smells of a missing test in the CatTests test fixture.

C#
[Test]
public void DeepClone_CatClone_DeepClonesCatOwner()
{
    CatOwner mockCatOwner = Substitute.ForPartsOf<CatOwner>("NotDanny", 9);
    Cat catUnderTest = new Cat(mockCatOwner, "Sir Bottomsworth", 5);

    catUnderTest.DeepClone();

    mockCatOwner.Received().DeepClone();
}

This test will still pass when the DeepClone method of the CatOwner class is broken, but it is not the responsibility of the CatTests class to ensure that the CatOwner class is functioning properly. It is responsible, however, for making sure that all of its reference types are deep cloned. All that needs to be done for this is to ensure that DeepClone is called on the CatOwner class when a Cat is cloned. CatOwner is not the only reference type that needs to be check, the Name also needs to be cloned. It's not possible to mock the String class, but the ReferenceEquals method can be used to ensure that the object is being cloned.

C#
[Test]
public void DeepClone_CatClone_DeepClonesName()
{
    CatOwner mockCatOwner = Substitute.For<CatOwner>("NotDanny", 9);
    Cat catUnderTest = new Cat(mockCatOwner, "Captain Whiskers", 7);
    Cat catClone = catUnderTest.DeepClone();

    bool referenceEquals = Object.ReferenceEquals(catUnderTest.Name, catClone.Name);

    Assert.False(referenceEquals);
}

The same test can also be replicated in the CatOwnerTests class.

C#
[Test]
public void DeepClone_CatOwnerClone_ClonesCatOwnerName()
{
    CatOwner catOwnerUnderTest = new CatOwner("Mr. Biscuits", 38);
    CatOwner catOwnerClone = catOwnerUnderTest.DeepClone();

    bool referenceEquals = object.ReferenceEquals(catOwnerUnderTest.Name, catOwnerClone.Name);

    Assert.False(referenceEquals);
}

All of these tests pass. To see them break, the copy constructors can be modified to only copy the references of a class instead of performing a deep clone as they should. Here is a modified version of the Cat copy constructor to show what an incorrect implementation would look like:

C#
protected Cat(Cat cat)
{
    if (cat == null)
        throw new ArgumentNullException("cat");

    //this.CatOwner = cat.CatOwner.DeepClone();
    //this.Name = String.Copy(cat.Name);
    this.CatOwner = cat.CatOwner;
    this.Name = cat.Name;

    this.Age = cat.Age;
}

When the tests are run with this constructor instead of the correct one, you can see both the DeepClone_CatClone_DeepClonesName and DeepClone_CatClone_DeepClonesCatOwner tests fail. The tests can also be shown to fail when a shallow clone is performed instead of a deep clone.

C#
public virtual CatOwner DeepClone()
{
    //return new CatOwner(this);
    return (CatOwner)this.MemberwiseClone(); // Please don't do this
}
C#
public virtual Cat DeepClone()
{ 
    //return new Cat(this); 
    return (Cat)this.MemberwiseClone();
}

If there were other reference types, those would also need to be tested in a similar way. While this can be repetitive, I do not know of a generic way to test that a deep clone recursively deep clones all of an object's properties (let me know if you can think of something that would work).

There is one thing that is left to be tested. When an object is cloned, the clone must also be identical to the original. One way to do this would be to go back through each test class and ensure all the properties match up (ideally with one test per property). An easier way to do this is to compare the data in memory of both the clone and the original and ensure that they are the same. This can be accomplished with serialization. Back in the IDeepCloneableTest class, there is a support method for converting an IDeepCloneable object into a byte array.

C#
private byte[] SerializeObjectToByteArray(T value)
{
    BinaryFormatter formatter = new BinaryFormatter();
    using (MemoryStream stream = new MemoryStream())
    {
        formatter.Serialize(stream, value);
        return stream.ToArray();
    }
}

In order for this to work, all classes implementing IDeepCloneable that are being tested must be marked with the Serializable attribute. If not, then the following test in the IDeepCloneableTests class will fail.

In the IDeepCloneableTest class, the next test is grabbing a new instance of IDeepCloneable, and converting it into a byte array. Then it clones the object using the DeepClone method and converts the clone into a byte array. The test will pass if both arrays of bytes are identical. This test will still pass when shallow clones of the objects are made, so it is only effective when used in conjunction with the other unit tests.

C#
[Test]
public void DeepClone_ClonedObject_HasIdentitcalByteStreamToOriginal()
{
    T objectUnderTest = this.GetCloneableObject();
    T clone = objectUnderTest.DeepClone(); 
    
    byte[] objectUnderTestAsBytes = this.SerializeObjectToByteArray(objectUnderTest);
    byte[] cloneAsBytes = this.SerializeObjectToByteArray(clone);

    Assert.AreEqual(objectUnderTestAsBytes, cloneAsBytes);
}

More importantly, the test will fail if the attributes of the object and clone do not match up. This can be verified by modifying the copy constructor of either the Cat or CatOwner class.

C#
protected CatOwner(CatOwner catOwner)
{
    if (catOwner == null)
        throw new ArgumentNullException("catOwner");

    this.Name = "This sure isn't the way to perform a deep clone.";
    //this.Name = String.Copy(catOwner.Name);
    this.Age = catOwner.Age;
}

Running the tests with the bad copy constructor confirms that the DeepClone_ClonedObject_HasIdentitcalByteStreamToOriginal test will not pass if an object is not properly cloned. Even with only one copy constructor changed, tests for both Cat and CatOwner will fail (because CatOwner is not stubbed out in the CatTests class. This is because the byte arrays are no longer identical for both objects.

It is important to note that the DeepClone_ClonedObject_HasIdentitcalByteStreamToOriginal will also fail if the GetCloneableObejct method returns an object that has any dependencies stubbed out with NSubstitute. Normally, this would be bad practice when writing a unit test, but sometimes exceptions must be made.

The full source code is attached to this article.

License

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

Share

About the Author

DannyFreeman
Software Developer
United States United States
I'm a student at UT Chattanooga studying Computer Science and Applied Mathematics.

Comments and Discussions

 
QuestionLimitation ? Pin
Dmitriy Gakh13-May-16 19:17
professionalDmitriy Gakh13-May-16 19:17 
AnswerRe: Limitation ? Pin
DannyFreeman13-May-16 20:10
professionalDannyFreeman13-May-16 20:10 
QuestionRe: Limitation ? Pin
Dmitriy Gakh13-May-16 20:58
professionalDmitriy Gakh13-May-16 20:58 
AnswerRe: Limitation ? Pin
DannyFreeman16-May-16 5:14
professionalDannyFreeman16-May-16 5:14 
SuggestionRe: Limitation ? Pin
Dmitriy Gakh16-May-16 7:28
professionalDmitriy Gakh16-May-16 7:28 
GeneralRe: Limitation ? Pin
DannyFreeman16-May-16 7:30
professionalDannyFreeman16-May-16 7:30 
QuestionWhy String.Copy? Pin
Daniele Rota Nodari13-May-16 0:06
MemberDaniele Rota Nodari13-May-16 0:06 
AnswerRe: Why String.Copy? Pin
DannyFreeman13-May-16 6:14
professionalDannyFreeman13-May-16 6:14 
GeneralRe: Why String.Copy? Pin
Daniele Rota Nodari15-May-16 21:01
MemberDaniele Rota Nodari15-May-16 21:01 

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.