Josh
Josh

Reputation: 8477

Update Two common functions to use Generics

Using Microsoft Unit Test Wizard, it creates Accessor objects if you need to test a non-public property in another project. Inside my Unit Tests I create helper functions so that I don't repeat the same code just in every Unit Test method. Currently I have two tests that are almost identical except one takes a standard object, and the other takes the Accessor version. Since the Accessor is based on the standard version I should be able to have one function and I assume I should be able to use Generics to accomplish. The issue is trying to retype and compile failures.

Here are the existing two functions:

// Common function to create a new test record with standard Account object
internal static void CreateAccount(out Account account, bool saveToDatabase)
{
    DateTime created = DateTime.Now;
    string createdBy = _testUserName;

    account = new Account(created, createdBy);

    account.Notes = Utilities.RandomString(1000);

    if (saveToDatabase)
        account.Create();
}

// Common function to create a new test record with Account_Accessor
internal static void CreateAccount(out Account_Accessor account, bool saveToDatabase)
{
    DateTime created = DateTime.Now;
    string createdBy = _testUserName;

    account = new Account_Accessor(created, createdBy);

    account.Notes = Utilities.RandomString(1000);

    if (saveToDatabase)
        account.Create();
}

I tried changing the signature to of a combined function to:

internal static void CreateAccount<T>(out T account, bool saveToDatabase) {...}

but couldn't get recast T properly to Account or Account_Accessor. Any suggestions?

Upvotes: 2

Views: 160

Answers (2)

zabulus
zabulus

Reputation: 2513

You should add constraint to the generic function, because of this two methods:

account.Notes = Utilities.RandomString(1000);
account.Create();

I suggest you to add some interface with this two methods and add inheritance from it to your two classes. Constraint should be as follows:

where T : YourNewInterface

About constraints you can read at http://msdn.microsoft.com/en-us/library/bb384067.aspx

UPDATE

public interface IAccount
    {
        string Notes { get; set; }
        void Create();
        void Init(DateTime created, string createdBy);
    }

public class Account : IAccount
{
    public string Notes
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public void IAccount.Create()
    {
        throw new NotImplementedException();
    }

    void IAccount.Init(DateTime created, string createdBy)
    {
        throw new NotImplementedException();
    }
}

public class Account_Accessor : IAccount
{

    string IAccount.Notes
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public void IAccount.Create()
    {
        throw new NotImplementedException();
    }

    void IAccount.Init(DateTime created, string createdBy)
    {
        throw new NotImplementedException();
    }
}


class Program
{
    internal static void CreateAccount<T>(out T account, bool saveToDatabase) where T : IAccount,new()
    {
        DateTime created = DateTime.Now;
        string createdBy = _testUserName;

        account = new T();
        account.Init(created, createdBy);

        account = (T)Activator.CreateInstance(typeof(T), new object[] { created, createdBy });

        account.Notes = Utilities.RandomString(1000);

        if (saveToDatabase)
            account.Create();
    }
    static void Main(string[] args)
    {
        Account acc;
        Account_Accessor acc2;
        CreateAccount(out acc, false);
        CreateAccount(out acc2, false);
    }
}

Here is some comments about my example:
1. I've replaced CreateInstance by adding new() constraint.
2. Because new() constraint can't have parameters because of .NET generic limitations, I've added Init() method to the IAccount interface.
3. Init method should not be called by client code of the Account class, that's why we define the method as private and explicitly for IAccount.
4. Because of new() constraint you should provide parameterless constructor for Account. If you do this, your client code should not call this parameterless ctor.

As for me I'd leave Activator.CreateInstance as is. It is good workaround for the limitations of generic new() constraint

Upvotes: 3

LightStriker
LightStriker

Reputation: 21004

Here's my take on making that method generic.

public abstract class BaseAccount
{
    public string Notes;

    public virtual void Create() { ... }
}

public class Account : BaseAccount { ... }

public class Account_Accessor : BaseAccount { ... }

internal static void CreateAccount<T>(out T account, bool saveToDatabase) where T : BaseAccount
{
    DateTime created = DateTime.Now;
    string createdBy = _testUserName;

    account = (T)Activator.CreateInstance(typeof(T), new object[] { created, createdBy });

    account.Notes = Utilities.RandomString(1000);

    if (saveToDatabase)
        account.Create();
}

I assume that Account and Account_Accessor are similar enough that they can share a similar class hiearchy, or could implement the same interface. In this example, I've provided an abstract class from which they both derive, but it is very easy to do it with an interface instead. However, the full implementation will have to be done in both class.

Knowing that, I can constraint the generic method so that T is only a child of BaseAccount. This way, I can access member from that base class without knowing the real type of T.

Upvotes: 0

Related Questions