Testing non-public methods in Orcas Beta1

Unless you've been living under a rock, you're probably aware that we shipped Visual Studio Orcas Beta1 last week. Here's Soma's blog post on the release which also has a link to the download. This is a huge milestone for us as the product that we've been carving out with love for the last year or so is coming together and you can see the final shape.

For the VSD team, unit testing for NetCF(which we blogged about here) is a big-ticket item and we're super excited that you're finally seeing it in its close-to-final form. 

Testing non-public methods in Beta1

If you try to generate unit tests for a non-public method in Beta1, you'll see a comment saying "Generation of unit tests for private methods of device projects not supported" . Don't panic - this is a temporary placeholder while we fixed a few issues with the code generation. You won't see this in the the next CTP/Beta for Orcas (as we've already fixed it internally). In the meantime, here's how you can easily work around this by writing a few lines of code and using the Smart Device Unit Test Framework.

In a nutshell, you can use the PrivateType and PrivateObject types found in the unit test framework to get at the ‘privates’ of your code. Let’s look at a detailed example

Step1: Lets generate some code

First of all lets generate some test code for below application which contains different kinds of non-public constructs. How to generate code can be found at https://msdn2.microsoft.com/en-us/library/ms364064(vs.80).aspx

using System;

using System.Collections.Generic;

using System.Text;

namespace BankAccountDemo.Business

{

    class BankAccount

    {

        // Properties

        private int _accountno;

        private float _currentBalance;

        private static float _bankAsset;

        private static int _newUniqueAccountno;

        private static int GetUniqueAccountNo

        {

            get {return _newUniqueAccountno++;}

        }

        private float CurrentBalance

        {

            get { return _currentBalance; }

            set { _currentBalance = value; }

        }

        // Constructors for new account

        private BankAccount(float initialBalance)

        {

            this._accountno = GetUniqueAccountNo;

            this._currentBalance = initialBalance;

        }

        // Methods

        private float DepositMoney(float depositAmount)

        {

            this.CurrentBalance += depositAmount;

           _bankAsset += depositAmount;

            return this.CurrentBalance;

        }

        private float WithdrawMoney(float withdrawAmount)

        {

            this.CurrentBalance -= withdrawAmount;

            _bankAsset -= withdrawAmount;

            return this.CurrentBalance;

        }

        private static float BankAsset()

        {

            return _bankAsset;

        }

    }

}

Step2: Add accessor class in Test Project to ease accessing the non-public code

Add a class to the test project (we call it BankAccountPrivateAccess in the example below). This class will contain the private accessor methods which can be used by testmethods to access non-public code

using System;

using System.Collections.Generic;

using System.Text;

using BankAccountDemo.Business;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestProject2

{

    /// <summary>

    /// Accessor Class for BankAccount

    /// </summary>

    public class BankAccountPrivateAccess

    {

        PrivateObject m_privateObject;

        static PrivateType m_privateType =

            new PrivateType("SmartDeviceProject2",

                "BankAccountDemo.Business.BankAccount");

        /// <summary>

        /// Constuctor initializing private object to invoke methods

        /// </summary>

        /// <param name="target"></param>

        public BankAccountPrivateAccess(object target)

        {

            m_privateObject = new PrivateObject(target,m_privateType);

        }

        /// <summary>

        /// Return target object of non-public class to be tested

        /// </summary>

        /// <param name="target"></param>

        public static object CreatePrivate(float initialBalance)

        {

            object[] args = new object[] { initialBalance };

            PrivateObject priv_obj =

                new PrivateObject("SmartDeviceProject2",

                    "BankAccountDemo.Business.BankAccount",

                    new System.Type[] { typeof(float) }, args);

            return priv_obj.Target;

        }

    }

}

Step3: Add Private Accessor methods and properties in above class to access non-public methods and properties

Add the following method to access method DepositMoney

        public float DepositMoney(float depositAmount)

        {

            object[] args = new object[] { depositAmount };

            return ((float)(m_privateObject.Invoke

                ("DepositMoney", new Type[] {typeof(float)}, args)));

        }

Add the following method to access static method BankAsset

        public static float BankAsset()

        {

            object[] args = new object[0];

            return ((float)(m_privateType.InvokeStatic

                ("BankAsset", new System.Type[0], args)));

        }

Add following method to access property CurrentBalance

       public float CurrentBalance

        {

            get

            {

                return ((float)(m_privateObject.GetProperty("CurrentBalance")));

            }

            set

            {

                m_privateObject.SetProperty("CurrentBalance", value);

            }

        }

Add following property to access static property GetUniqueAccountNo

       public static int GetUniqueAccountNo

        {

            get

            {

                return ((int)(m_privateType.GetStaticProperty("GetUniqueAccountNo")));

            }

        }

You can use similar code to access any other member you have.

Step4: Modify the Generated Test Code to call methods of accessor class

Add following code to Test Deposit Money Test method

    // TODO: Initialize to an appropriate value

    object param0 = BankAccountPrivateAccess.CreatePrivate(5000);

    // TODO: Initialize to an appropriate value

    BankAccountPrivateAccess target = new BankAccountPrivateAccess(param0);

    // TODO: Initialize to an appropriate value

    float depositAmount = 0F;

    // TODO: Initialize to an appropriate value

    float expected = 0F;

    float actual;

    actual = target.DepositMoney(depositAmount);

    Assert.AreEqual(expected, actual);

    Assert.Inconclusive("Verify the correctness of this test method.");

Modify the other testmethods to use your accessor class as well.

Voila! Your code is now testable using Orcas. Remember that this workaround is temporary – you won’t need to do any of this the next time you get Orcas bits.