Migrate MSUnit tests Accessors to PrivateObjects
MSUnits tests are fussy at the best of times and don’t run in a number of popular environments, such as Travis. If you have invested a lot into white-box MSUnit tests already and need to give your tests better compatability (with Visual Studio Online/Team Services for example) then you can move away from using Accessor
types to using PrivateObject
. For more pros/cons, scroll to the bottom of the article.
These examples are in VB and C#:
How to
Find places where you create Accessor objects:
Dim target As New MyThing_Accessor() 'Old VB
var target = new MyThing_Accessor(); //Old C#
Change them to create a normal object instance and add a PrivateObject instantiator below:
'New VB
Dim target As New MyThing()
Dim pTarget As New PrivateObject(target)
//New C#
var target = new MyThing();
var pTarget = new PrivateObject(target);
Now find the places you call the methods through your _Accessor
class:
Dim actual As String = target.FullActiveFileNameWithoutExtension(mockWorkbook) 'Old VB
var actual = target.FullActiveFileNameWithoutExtension(mockWorkbook); //Old C#
Change them to call pTarget.Invoke
with the string name of your method, passing all parameters afterwards (in ParamArray
style):
'New VB
Dim actual As String = CStr(pTarget.Invoke("FullActiveFileNameWithoutExtension", mockWorkbook))
//New C#
var actual = (string)pTarget.Invoke("FullActiveFileNameWithoutExtension", mockWorkbook)
Note that because Invoke
is dynamic, we no longer have a guarantee (at design time) of what Type will be returned, so you need to convert or cast it to the expected type. We do this above with CStr()
or (string)
.
Anywhere you need to get/set a private field or property, you can use pTarget.SetFieldOrProperty
like this:
'VB
Dim target As New MyThing()
Dim pTarget As New PrivateObject(target)
pTarget.SetFieldOrProperty("LastMasterFileName", "C:\Users\Bob\Documents\MyWorkbook.xlsm")
...
Simple invocation example
Here’s a simple test around a method for getting file paths for Excel workbooks (it has to work for local and remote paths, and is basically a wrapper around some Path
methods - I’m not reinventing the wheel, honest). Extra comments added to show changes.
This is in VB (C# conversion is exercise for the reader):
Before
<TestMethod(), _
DeploymentItem("MyAddIn.dll")> _
Public Sub FullActiveFileNameWithoutExtension_WindowsFileName()
'Set up the accessor to let us test the private method
Dim target As MyThing_Accessor = New MyThing_Accessor()
'Create mock workbook to pass into the method
Dim mockWorkbook As New MockWorkbook("C:\Users\Bob\Documents\MyWorkbook.xlsm")
'Test
Dim expected As String = "C:\Users\Bob\Documents\MyWorkbook"
Dim actual As String = target.FullActiveFileNameWithoutExtension(mockWorkbook)
Assert.AreEqual(expected, actual)
End Sub
After
<TestMethod(), _
DeploymentItem("MyAddIn.dll")> _
Public Sub FullActiveFileNameWithoutExtension_WindowsFileName()
'Create the object using its regular constructor
Dim target As MyThing = New MyThing()
'Build the private object reflector
Dim pTarget As New PrivateObject(target)
'Create mock workbook to pass into the method
Dim mockWorkbook As New MockWorkbook("C:\Users\Bob\Documents\MyWorkbook.xlsm")
'Call the private method using Invoke on our pTarget
Dim expected As String = "C:\Users\Bob\Documents\MyWorkbook"
Dim actual As String = CStr(pTarget.Invoke("FullActiveFileNameWithoutExtension", mockWorkbook))
Assert.AreEqual(expected, actual)
End Sub
Longer example
Here’s a more complex example with field changes and more parameters in invocation:
Before
<TestMethod(), _
DeploymentItem("MyAddIn.dll")> _
Public Sub WorkingCopyFileName_FromWindowsFileName_ByKnowingLastMasterFileName()
Dim target As MyThing_Accessor = New MyThing_Accessor()
target.LastMasterFileName = "C:\Users\Bob\Documents\MyWorkbook.xlsm"
'Active filename is usually obtained from FullActiveFileNameWithoutExtension
Dim activeFileName As String = "C:\Users\Bob\Documents\MyWorkbook_abcd1234"
Dim expected As String = "C:\Users\Bob\Documents\MyWorkbook.xlsm"
Dim actual As String = target.WorkingCopyFileURI(activeFileName, Nothing)
Assert.AreEqual(expected, actual)
End Sub
After
<TestMethod(), _
DeploymentItem("MyAddIn.dll")> _
Public Sub WorkingCopyFileName_FromWindowsFileName_ByKnowingLastMasterFileName()
Dim target As MyThing = New MyThing()
Dim pTarget As New PrivateObject(target)
'Instead of setting field directly, use SetFieldOrProperty on PrivateObject
pTarget.SetFieldOrProperty("LastMasterFileName", "C:\Users\Bob\Documents\MyWorkbook.xlsm")
'Active filename is usually obtained from FullActiveFileNameWithoutExtension
Dim activeFileName As String = "C:\Users\Bob\Documents\MyWorkbook_abcd1234"
'Pass all parameters to WorkingCopyFileURI after the method name
Dim expected As String = "C:\Users\Bob\Documents\MyWorkbook.xlsm"
Dim actual As String = CStr(pTarget.Invoke("WorkingCopyFileURI", activeFileName, Nothing))
Assert.AreEqual(expected, actual)
End Sub
Conclusion
Pros:
- Better compatability
- More obvious use of reflection
- No more broken Accessor objects when changing environments
Cons:
- Less obvious method call
- Can’t go to definition as easily
- Have to manually convert/cast returned objects
Good luck, have fun…