다음을 통해 공유


Writing automated test cases for VSTO application

The VSTO Add-in is used to work with office applications. The major concern with it is to write automated unit test cases and code coverage, which is generally an important expectation from projects.

We will look into the approaches those can be used to write the test case.

 

Approach 1: Access COMAddin.Object to share the objects between VSTO and non-VSTO projects.

The first approach to discuss is using some of the good technique mentioned by Andrew Whitechapelin hisblog. (https://blogs.msdn.com/b/andreww/archive/2007/01/15/vsto-add-ins-comaddins-and-requestcomaddinautomationservice.aspx)

Although the article does not talk about test automation, the mentioned technique is quite useful to write the test cases.

Now let’s take a simple example. Say we have GetDocumentName method in our Addin project as below.

public class AddinHelper

{

public string GetDocumentName ()

        {

            return Globals.ThisAddIn.Application.ActiveDocument.Name;

        }

}

 

If we write a test around this method, by creating instance of AddinHelper class and try to execute the same, it will fail. That is because the test project would be running in different process than word and as the method is trying to access add-in object to get its properties, test application process cannot fetch them.

The best way would be, add-in to expose the instance of the AddinHelper class to external applications.

Below are few basic steps required to follow this approach:

1) Create comVisible Interface.

2) Have a comVisible class implementing above interface whose instance would be exposed to external applications

3) Override RequestComAddInAutomationService method in add-in which actually returns this step 2 class instance.

4) Access the exposed object using ComAddin.Object as Interface type in test project.

Now let’s have a look at above steps in detail-

The VSTO add-in has RequestComAddInAutomationService method to override which can share comVisible objects using ComAddin.Object property. When external application uses this object to call addin methods, as it is comVisible, the call is forwarded to word process and the code gets executed.

Below code will expose helper class instance (ThisAddin.cs).

private AddinHelper comAddinObject;

/// <summary>

/// Expose the object

/// </summary>

/// <returns></returns>

protected override object RequestComAddInAutomationService()

{

       if (comAddinObject == null)

       {

       comAddinObject = new AddinHelper();

}

return comAddinObject;

}

Also this shared com object can only be accessed as interface type, hence AddinHelper must implement interface and mark it as ComVisible=true as below.

[ComVisible(true)]

[Guid("8FDE2B7C-F970-46ED-A1FE-5AAE923C7E5B")]

[InterfaceType(ComInterfaceType.InterfaceIsDual)]

public class IAddinHelper

{

string GetDocumentName ();

}

 

[ComVisible(true)]

[ClassInterface(ClassInterfaceType.None)]

[Guid("F2E9DF5C-B4D1-44E0-B20F-851383EA6C9A")]

public class AddinHelper : IAddinHelper

{

public string GetDocumentName ()

        {

            return Globals.ThisAddIn.Application.ActiveDocument.Name;

        }

}

· Making ComVisible

Setting [ComVisible(true)] attribute indicates that the class is visible to COM. It makes sure the type library is created and can be registered. Typelib is metadata of classes and interfaces of ComVisible types.

· Why use GUID?

These values are used by COM clients to locate the assembly that implements the COM class. The interface Guid will be installed at HKEY_CLASSES_ROOT\Interface and the class GUID is the class id installed at HKEY_CLASSES_ROO\CLSID

e.g. HKEY_CLASSES_ROOT\Interface\{8FDE2B7C-F970-46ED-A1FE-5AAE923C7E5B}\

e.g. HKEY_CLASSES_ROOT\CLSID\{ F2E9DF5C-B4D1-44E0-B20F-851383EA6C9A }\

Hence specifying GUID makes it easy to locate these entries in the registry.

· What is InterfaceType?

The ClassInterfaceType.None explains no other interface should be generated than the one defined and ComInterfaceType.InterfaceIsDual explains that the interface is exposed as dual, enabling early and late binding.

 

To get the registry entries, select “Register for COM Interop” checkbox on add-in project properties.

 

Now let’s write our simple test case.

Create test project and add addin project’s dll reference. The limitation with VSTO applications is that cannot be referred as a “Project Reference.”

 

[TestMethod]

public void TestGetDocumentName()

{

Application wordApp = new Application();

Document wordDocToTest = wordApp.Documents.Open(testDoc);

COMAddIns comAddins = wordApp.COMAddIns;

comAddin = comAddins.Item("TestVSTOAddin");

IAddinHelper comobj = (IAddinHelper)comAddin.Object;

string name1 = comobj.GetDocumentName();

wordDocToTest.Saved = true;

wordDocToTest.Close();

       wordApp.Quit();

       wordDocToTest = null;

Assert.IsFalse(string.IsNullOrEmpty(name1));

}

This test method creates an instance of the application and opens the document. At this moment the add-in gets hooked to the application and would expose/share the COM visible object to non VSTO application (in our case Test Project) through Comaddin.Object property.

 

Get the exact comAddin from applications’ COMAddIns collection by specifying add-in dll name. Then use Object property on found add-in and the com visible object would be available to make direct calls to its methods at compile time (early binding).

 

The calls made using this object are passed to COM application and hence code gets covered under addin.dll.

 

Run the test case and let’s have a look at test case result and code coverage!

 

 

 

Few important points which are observed and would be helpful

1) Registry entries: As mentioned above the comvisible types are registered. If we deploy the add-in the setup makes these registry entries. The same happens when we build the project and the registry gets cleared on cleaning the project/solution. Sometimes you might want to run visual studio as “Administrator” to build the solution, else it may throw permission errors preventing reg.

2)  Sharing COMAddin.Object for use by multiple test cases:

As shown in above test case we are creating application, opening document and using COMAddin.Object. We might want to use this object in running multiple cases. We can move these steps in ClassIntilize and share the COMAddin object.

 

So the above Test case will look like-

        public static Document wordDocToTest = null;

        public static string testDoc = @"D:\Test.docx";

        static IAddinHelper comobj = null;

        static Application wordApp = null;

        [ClassInitialize()]

        public static void ClassInitialize(TestContext tc)

        {

       //start the VSTO Word document

            wordApp = new Application();

            wordDocToTest = wordApp.Documents.Open(testDoc);

            COMAddIns comAddins = wordApp.COMAddIns;

            COMAddIn comAddin = comAddins.Item("TestVSTOAddin");

            comobj = (IAddinHelper)comAddin.Object;

        }

        [TestMethod]

        public void TestGetData()

        {

            string data = comobj.GetData(wordDocToTest);

            Assert.IsNotNull(data);

        }

        [TestMethod]

        public void TestGetDocumentName()

        {

            string name1 = comobj.GetDocumentName();

            Assert.IsTrue(!string.IsNullOrEmpty(name1));

        }

        [ClassCleanup()]

        public static void ClassCleanup()

    {

            wordDocToTest.Saved = true;

            wordDocToTest.Close();

            wordApp.Quit();

        }

 

 When we run these test cases they all run in separate thread and in STA. Hence to share this object we need to update the test settings to “LTA”. As the VS does not have UI option to update this setting, open “Local.testsettings” as XML and add below entry.

< Execution

  < ExecutionThread apartmentState = " MTA " />

...

 

update testconfig in VS less than 2010 as

<TestRunConfiguration name="Local Test Run" 
< ExecutionThread apartmentState = " MTA " />
...

It is also observed that, to apply these settings we need to restart the Visual Studio.

3) Implementing StandardOleMarshalObject:

The addin works on main UI thread and calls from test application will not execute on this main UI thread as they are in different process. So if addin has UI programming (e.g creating task pane) it will not be carried out by COM. We may end up with error as “The calling thread must be STA, because many UI components require this.”

To make sure that COM accepts call from exposed object, which is created by addin and is from STA, addin object must implement StandardOleMarshalObject.

For more details please blog by Andrew

https://blogs.msdn.com/b/andreww/archive/2008/08/11/why-your-comaddin-object-should-derive-from-standardolemarshalobject.aspx

 

Please find the attached sample for reference.

 

 

Approach 2: Direct call to Add-in methods

Say we have created helper class for add-in. If this class can be independently instantiated or its static methods can be called then our job is simple!

Let’s take a simple example.

AddinHelper.cs

public class AddinHelper

    {

        public static string GetDocumentName(Document doc)

        {

            return doc.Name;

        }

    }

 

To write the test case of above method is very easy as below

[TestMethod]

public void TestGetDocumentName()

{

Application wordApp = new Application();

Document wordDocToTest = wordApp.Documents.Open(testDoc);

string name1 = AddinHelper.GetDocumentName(wordDocToTest);

Assert.IsFalse(string.IsNullOrEmpty(name1));

 }

The drawback with this approach is that, if helper class is part add-in dll and if it uses internal variables like Globals.ThisAddin or types instantiated by add-in class, then above test case will fail. Because test would execute the method in separate process than add-in or word process and cannot access those out of process variables.

If you can have classes without this dependency, then we are good to go!

 

TestVSTOAddin.zip

Comments

  • Anonymous
    August 17, 2010
    This is v.cool. Good job Varsha! I will post a redirection link on my blog for this one.

  • Anonymous
    August 17, 2010
    Good article!! But I have one question?? Please refer the code part of your article.

  1.       Sharing COMAddin.Object for use by multiple test cases: .. .. ..        public static string testDoc = @"D:Test.docx"; Question: If you use static path like this, how the test would pass in the generic way on server. This seems a dependency for this unit test which enforces the manual configuration on the server. I think unit test should not go out of class boundaries or include network, database, and storage.
  • Anonymous
    August 24, 2010
    Thanks Praveen for the comments, I have added this path just to keep simplicity of example. If its going to run on server, I agree to your point. In that case we can either have it as embedded doc or we can just add new doc to application by application.Documents.Add(...

  • Anonymous
    October 10, 2013
    Hi Varsha, I want to do something similar. Instead of Addin, i want to write automated unit test cases for word customization using VSTO.

  • Anonymous
    May 08, 2015
    Hi varsha, can we use this method to do automation of vsto application for excel instead of add- in Regard,s, P

  • Anonymous
    December 01, 2015
    will you provide the working code sample. it will be very helpful for me.

  • Anonymous
    December 02, 2015
    will you please Guide me how to test the power point method insert new slide. Because it is a void method.