Abstracting and unit testing lookups in Excel table
$begingroup$
Background
I have a vba solution that I use to ingest investment text reports and reformat them for analysis in Excel. It works, but the macros involve a lot of direct manipulation of Excel objects, and have no unit-testing.
After finding RubberDuck, and reading several years' worth of excellent posts from @MathieuGuindon, I've decided to re-write the "brute force"-heavy solution as a way to learn these new concepts and techniques.
When ingesting from a report, I also pull additional attributes from excel tables. I'm beginning my re-write with those lookup tables. The first of which I'm submitting here.
Initial goals:
- Programming to Interfaces not classes
- Making Services and Proxies rather than direct access to Excel sheets and ranges
- Using the PredeclaredId attribute to enable a Create method
- Thorough unit testing
Apart from general review, I also have some specific questions, which I'll post following the code.
Code
IAssetTableProxy
-- abstracts reference to the "physical" excel table's data rows
'@Folder("Services.Interfaces")
Option Explicit
Public Function GetAssetTableData() As Variant()
End Function
AssetTableProxy
-- Implementation
'@Folder("Services.Proxies")
Option Explicit
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim tblName As String
tblName = "AssetInfoTable"
IAssetTableProxy_GetAssetTableData = Worksheets(Range(tblName).Parent.Name).ListObjects(tblName).DataBodyRange.value
End Function
AssetInfo
-- a class to handle the three values for each row: Desc, Ticker, Type
'@PredeclaredId
'@Folder("Services")
Option Explicit
Private Type TAssetInfo
Desc As String
Ticker As String
AssetType As String
End Type
Private this As TAssetInfo
Public Property Get Desc() As String
Desc = this.Desc
End Property
Friend Property Let Desc(ByVal value As String)
this.Desc = value
End Property
Public Property Get Ticker() As String
Ticker = this.Ticker
End Property
Friend Property Let Ticker(ByVal value As String)
this.Ticker = value
End Property
Public Property Get AssetType() As String
AssetType = this.AssetType
End Property
Friend Property Let AssetType(ByVal value As String)
this.AssetType = value
End Property
Public Property Get Self() As AssetInfo
Set Self = Me
End Property
Public Function Create(ByVal theDesc As String, ByVal theTicker As String, ByVal theAssetType As String) As AssetInfo
With New AssetInfo
.Desc = theDesc
.Ticker = theTicker
.AssetType = theAssetType
Set Create = .Self
End With
End Function
IAssetInfoService
-- holds a collection of AssetInfo objects and
provides the needed lookups to data from AssetTableProxy
'@Folder("Services.Interfaces")
Option Explicit
Public Function Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
End Function
Public Function GetAssetTypeForDesc(ByVal Desc As String) As String
End Function
Public Function GetTickerForDesc(ByVal Desc As String) As String
End Function
AssetInfoService
-- implementation
'@PredeclaredId
'@Folder("Services")
Option Explicit
Option Base 1
Implements IAssetInfoService
Private Type TAssetsTable
AssetColl As Collection
End Type
Private this As TAssetsTable
Friend Property Get Assets() As Collection
Set Assets = this.AssetColl
End Property
Friend Property Set Assets(ByRef coll As Collection)
Set this.AssetColl = coll
End Property
Public Property Get Self() As IAssetInfoService
Set Self = Me
End Property
Public Function IAssetInfoService_Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
Dim twoDArr() As Variant
twoDArr = assetTbl.GetAssetTableData
With New AssetInfoService
Dim tempAsset As AssetInfo
Dim tempColl As Collection
Set tempColl = New Collection
Dim rw As Long
For rw = 1 To UBound(twoDArr, 1)
Set tempAsset = AssetInfo.Create(twoDArr(rw, 1), twoDArr(rw, 2), twoDArr(rw, 3))
tempColl.Add tempAsset, key:=tempAsset.Desc
Next rw
Set .Assets = tempColl
Set IAssetInfoService_Create = .Self
End With
End Function
Public Function IAssetInfoService_GetAssetTypeForDesc(ByVal Desc As String) As String
Dim tempTp As String
If Exists(this.AssetColl, Desc) Then
tempTp = this.AssetColl(Desc).AssetType
Else
tempTp = "Unknown Asset"
End If
IAssetInfoService_GetAssetTypeForDesc = tempTp
End Function
Public Function IAssetInfoService_GetTickerForDesc(ByVal Desc As String) As String
Dim tempTicker As String
If Exists(this.AssetColl, Desc) Then
tempTicker = this.AssetColl(Desc).Ticker
Else
tempTicker = "Unknown Asset"
End If
IAssetInfoService_GetTickerForDesc = tempTicker
End Function
Private Function Exists(ByRef coll As Collection, ByRef key As String) As Boolean
On Error GoTo ErrHandler
coll.Item key
Exists = True
ErrHandler:
End Function
Unit Testing
AssetTableTestProxy
-- proxy implementation for testing w/o dependency on actual excel table
'@Folder("Services.Proxies")
Option Explicit
Option Base 1
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim twoDArr(1 To 3, 1 To 3) As Variant
twoDArr(1, 1) = "Asset1"
twoDArr(1, 2) = "Tick1"
twoDArr(1, 3) = "Type1"
twoDArr(2, 1) = "Asset2"
twoDArr(2, 2) = "Tick2"
twoDArr(2, 3) = "Type2"
twoDArr(3, 1) = "Asset3"
twoDArr(3, 2) = "Tick3"
twoDArr(3, 3) = "Type3"
IAssetTableProxy_GetAssetTableData = twoDArr
End Function
TestAssetInfoService
-- Unit tests for Asset Info Service
Option Explicit
Option Private Module
'@TestModule
'@Folder("Tests")
Private Assert As Object
Private Fakes As Object
Private assetTbl As IAssetTableProxy
'@ModuleInitialize
Public Sub ModuleInitialize()
'this method runs once per module.
Set Assert = CreateObject("Rubberduck.AssertClass")
Set Fakes = CreateObject("Rubberduck.FakesProvider")
Set assetTbl = New AssetTableTestProxy
End Sub
'@ModuleCleanup
Public Sub ModuleCleanup()
'this method runs once per module.
Set Assert = Nothing
Set Fakes = Nothing
Set assetTbl = Nothing
End Sub
'@TestInitialize
Public Sub TestInitialize()
'this method runs before every test in the module.
End Sub
'@TestCleanup
Public Sub TestCleanup()
'this method runs after every test in the module.
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetTicker()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tick As String
tick = tbl.GetTickerForDesc("Asset2")
'Assert:
Assert.AreEqual "Tick2", tick, "Tick was: " & tick
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetAssetType()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim assetTp As String
assetTp = tbl.GetAssetTypeForDesc("Asset2")
'Assert:
Assert.AreEqual "Type2", assetTp, "AssetTp was: " & assetTp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetNotInTable_GetUnknownAssetMsg()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tp As String
tp = tbl.GetAssetTypeForDesc("unsub")
'Assert:
Assert.AreEqual "Unknown Asset", tp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Module1
-- additional sub to play around with functions
Option Explicit
Sub TestAssetInfoTable()
Dim assetTbl As IAssetTableProxy
Dim testAssetTbl As AssetTableTestProxy
Set assetTbl = New AssetTableProxy
Set testAssetTbl = New AssetTableTestProxy
Dim assetSvc As IAssetInfoService
Dim testAssetSvc As IAssetInfoService
Set assetSvc = AssetInfoService.IAssetInfoService_Create(assetTbl)
Set testAssetSvc = AssetInfoService.IAssetInfoService_Create(testAssetTbl)
Dim tp As String
Dim tick As String
tp = assetSvc.GetAssetTypeForDesc("AMAZON COM INC (AMZN)")
tick = assetSvc.GetTickerForDesc("AMAZON COM INC (AMZN)")
MsgBox ("Real Svc: tp=" & tp & "; tick=" & tick)
tp = testAssetSvc.GetAssetTypeForDesc("Asset3")
tick = testAssetSvc.GetTickerForDesc("Asset3")
MsgBox ("Test Svc: tp=" & tp & "; tick=" & tick)
End Sub
Specific questions:
- I initially had the "proxy" logic in the service class. But it felt like I was duplicating too many functions when I created the
AssetInfoTestService
class. Breaking it out toAssetTableProxy
andAssetTableTestProxy
allowed me to keep only one version of the service functions. But is this carrying things (abstraction?) too far?
Learning about interfaces, I believe I understand the following pieces:
- the contract created by each Function mentioned in the interface;
- the requisite coding of corresponding Interface_Function in the implementing class;
- the dimm-ing of class var "as Interface"; and
- accessing the functions with
classVar.Function
- However there seems to be an exception here. In
TestAssetInfoTable
I dim assetSvc asIAssetInfoService
. That interface has a Create function, and in the concrete class, I haveIAssetInfoService_Create
defined. But when I try to callAssetInfoService.Create(…)
I get a compile error that only clears when I change toAssetInfoService.IAssetInfoService_Create
. What am I missing there?
- However there seems to be an exception here. In
I see the "Option Base 1" thing. Since leave C pointers long ago, I haven't really had a religious belief one way or the other on 0- vs 1-based arrays. I went with it here, because when I began playing with the (extremely handy)
multiDimArray = Range
I noted the returned arrays are 1-based. And I kept screwing myself up between coding for those, and coding for my own 0-based ones. So I just opted to go all 1-based. Rubberduck Code Inspections do always throw that decision back in my face though, so let me ask here: are compelling reasons to not do that, or work arounds/tips for themultiDimArray = Range
1-based thing?
vba excel rubberduck
New contributor
$endgroup$
add a comment |
$begingroup$
Background
I have a vba solution that I use to ingest investment text reports and reformat them for analysis in Excel. It works, but the macros involve a lot of direct manipulation of Excel objects, and have no unit-testing.
After finding RubberDuck, and reading several years' worth of excellent posts from @MathieuGuindon, I've decided to re-write the "brute force"-heavy solution as a way to learn these new concepts and techniques.
When ingesting from a report, I also pull additional attributes from excel tables. I'm beginning my re-write with those lookup tables. The first of which I'm submitting here.
Initial goals:
- Programming to Interfaces not classes
- Making Services and Proxies rather than direct access to Excel sheets and ranges
- Using the PredeclaredId attribute to enable a Create method
- Thorough unit testing
Apart from general review, I also have some specific questions, which I'll post following the code.
Code
IAssetTableProxy
-- abstracts reference to the "physical" excel table's data rows
'@Folder("Services.Interfaces")
Option Explicit
Public Function GetAssetTableData() As Variant()
End Function
AssetTableProxy
-- Implementation
'@Folder("Services.Proxies")
Option Explicit
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim tblName As String
tblName = "AssetInfoTable"
IAssetTableProxy_GetAssetTableData = Worksheets(Range(tblName).Parent.Name).ListObjects(tblName).DataBodyRange.value
End Function
AssetInfo
-- a class to handle the three values for each row: Desc, Ticker, Type
'@PredeclaredId
'@Folder("Services")
Option Explicit
Private Type TAssetInfo
Desc As String
Ticker As String
AssetType As String
End Type
Private this As TAssetInfo
Public Property Get Desc() As String
Desc = this.Desc
End Property
Friend Property Let Desc(ByVal value As String)
this.Desc = value
End Property
Public Property Get Ticker() As String
Ticker = this.Ticker
End Property
Friend Property Let Ticker(ByVal value As String)
this.Ticker = value
End Property
Public Property Get AssetType() As String
AssetType = this.AssetType
End Property
Friend Property Let AssetType(ByVal value As String)
this.AssetType = value
End Property
Public Property Get Self() As AssetInfo
Set Self = Me
End Property
Public Function Create(ByVal theDesc As String, ByVal theTicker As String, ByVal theAssetType As String) As AssetInfo
With New AssetInfo
.Desc = theDesc
.Ticker = theTicker
.AssetType = theAssetType
Set Create = .Self
End With
End Function
IAssetInfoService
-- holds a collection of AssetInfo objects and
provides the needed lookups to data from AssetTableProxy
'@Folder("Services.Interfaces")
Option Explicit
Public Function Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
End Function
Public Function GetAssetTypeForDesc(ByVal Desc As String) As String
End Function
Public Function GetTickerForDesc(ByVal Desc As String) As String
End Function
AssetInfoService
-- implementation
'@PredeclaredId
'@Folder("Services")
Option Explicit
Option Base 1
Implements IAssetInfoService
Private Type TAssetsTable
AssetColl As Collection
End Type
Private this As TAssetsTable
Friend Property Get Assets() As Collection
Set Assets = this.AssetColl
End Property
Friend Property Set Assets(ByRef coll As Collection)
Set this.AssetColl = coll
End Property
Public Property Get Self() As IAssetInfoService
Set Self = Me
End Property
Public Function IAssetInfoService_Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
Dim twoDArr() As Variant
twoDArr = assetTbl.GetAssetTableData
With New AssetInfoService
Dim tempAsset As AssetInfo
Dim tempColl As Collection
Set tempColl = New Collection
Dim rw As Long
For rw = 1 To UBound(twoDArr, 1)
Set tempAsset = AssetInfo.Create(twoDArr(rw, 1), twoDArr(rw, 2), twoDArr(rw, 3))
tempColl.Add tempAsset, key:=tempAsset.Desc
Next rw
Set .Assets = tempColl
Set IAssetInfoService_Create = .Self
End With
End Function
Public Function IAssetInfoService_GetAssetTypeForDesc(ByVal Desc As String) As String
Dim tempTp As String
If Exists(this.AssetColl, Desc) Then
tempTp = this.AssetColl(Desc).AssetType
Else
tempTp = "Unknown Asset"
End If
IAssetInfoService_GetAssetTypeForDesc = tempTp
End Function
Public Function IAssetInfoService_GetTickerForDesc(ByVal Desc As String) As String
Dim tempTicker As String
If Exists(this.AssetColl, Desc) Then
tempTicker = this.AssetColl(Desc).Ticker
Else
tempTicker = "Unknown Asset"
End If
IAssetInfoService_GetTickerForDesc = tempTicker
End Function
Private Function Exists(ByRef coll As Collection, ByRef key As String) As Boolean
On Error GoTo ErrHandler
coll.Item key
Exists = True
ErrHandler:
End Function
Unit Testing
AssetTableTestProxy
-- proxy implementation for testing w/o dependency on actual excel table
'@Folder("Services.Proxies")
Option Explicit
Option Base 1
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim twoDArr(1 To 3, 1 To 3) As Variant
twoDArr(1, 1) = "Asset1"
twoDArr(1, 2) = "Tick1"
twoDArr(1, 3) = "Type1"
twoDArr(2, 1) = "Asset2"
twoDArr(2, 2) = "Tick2"
twoDArr(2, 3) = "Type2"
twoDArr(3, 1) = "Asset3"
twoDArr(3, 2) = "Tick3"
twoDArr(3, 3) = "Type3"
IAssetTableProxy_GetAssetTableData = twoDArr
End Function
TestAssetInfoService
-- Unit tests for Asset Info Service
Option Explicit
Option Private Module
'@TestModule
'@Folder("Tests")
Private Assert As Object
Private Fakes As Object
Private assetTbl As IAssetTableProxy
'@ModuleInitialize
Public Sub ModuleInitialize()
'this method runs once per module.
Set Assert = CreateObject("Rubberduck.AssertClass")
Set Fakes = CreateObject("Rubberduck.FakesProvider")
Set assetTbl = New AssetTableTestProxy
End Sub
'@ModuleCleanup
Public Sub ModuleCleanup()
'this method runs once per module.
Set Assert = Nothing
Set Fakes = Nothing
Set assetTbl = Nothing
End Sub
'@TestInitialize
Public Sub TestInitialize()
'this method runs before every test in the module.
End Sub
'@TestCleanup
Public Sub TestCleanup()
'this method runs after every test in the module.
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetTicker()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tick As String
tick = tbl.GetTickerForDesc("Asset2")
'Assert:
Assert.AreEqual "Tick2", tick, "Tick was: " & tick
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetAssetType()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim assetTp As String
assetTp = tbl.GetAssetTypeForDesc("Asset2")
'Assert:
Assert.AreEqual "Type2", assetTp, "AssetTp was: " & assetTp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetNotInTable_GetUnknownAssetMsg()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tp As String
tp = tbl.GetAssetTypeForDesc("unsub")
'Assert:
Assert.AreEqual "Unknown Asset", tp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Module1
-- additional sub to play around with functions
Option Explicit
Sub TestAssetInfoTable()
Dim assetTbl As IAssetTableProxy
Dim testAssetTbl As AssetTableTestProxy
Set assetTbl = New AssetTableProxy
Set testAssetTbl = New AssetTableTestProxy
Dim assetSvc As IAssetInfoService
Dim testAssetSvc As IAssetInfoService
Set assetSvc = AssetInfoService.IAssetInfoService_Create(assetTbl)
Set testAssetSvc = AssetInfoService.IAssetInfoService_Create(testAssetTbl)
Dim tp As String
Dim tick As String
tp = assetSvc.GetAssetTypeForDesc("AMAZON COM INC (AMZN)")
tick = assetSvc.GetTickerForDesc("AMAZON COM INC (AMZN)")
MsgBox ("Real Svc: tp=" & tp & "; tick=" & tick)
tp = testAssetSvc.GetAssetTypeForDesc("Asset3")
tick = testAssetSvc.GetTickerForDesc("Asset3")
MsgBox ("Test Svc: tp=" & tp & "; tick=" & tick)
End Sub
Specific questions:
- I initially had the "proxy" logic in the service class. But it felt like I was duplicating too many functions when I created the
AssetInfoTestService
class. Breaking it out toAssetTableProxy
andAssetTableTestProxy
allowed me to keep only one version of the service functions. But is this carrying things (abstraction?) too far?
Learning about interfaces, I believe I understand the following pieces:
- the contract created by each Function mentioned in the interface;
- the requisite coding of corresponding Interface_Function in the implementing class;
- the dimm-ing of class var "as Interface"; and
- accessing the functions with
classVar.Function
- However there seems to be an exception here. In
TestAssetInfoTable
I dim assetSvc asIAssetInfoService
. That interface has a Create function, and in the concrete class, I haveIAssetInfoService_Create
defined. But when I try to callAssetInfoService.Create(…)
I get a compile error that only clears when I change toAssetInfoService.IAssetInfoService_Create
. What am I missing there?
- However there seems to be an exception here. In
I see the "Option Base 1" thing. Since leave C pointers long ago, I haven't really had a religious belief one way or the other on 0- vs 1-based arrays. I went with it here, because when I began playing with the (extremely handy)
multiDimArray = Range
I noted the returned arrays are 1-based. And I kept screwing myself up between coding for those, and coding for my own 0-based ones. So I just opted to go all 1-based. Rubberduck Code Inspections do always throw that decision back in my face though, so let me ask here: are compelling reasons to not do that, or work arounds/tips for themultiDimArray = Range
1-based thing?
vba excel rubberduck
New contributor
$endgroup$
add a comment |
$begingroup$
Background
I have a vba solution that I use to ingest investment text reports and reformat them for analysis in Excel. It works, but the macros involve a lot of direct manipulation of Excel objects, and have no unit-testing.
After finding RubberDuck, and reading several years' worth of excellent posts from @MathieuGuindon, I've decided to re-write the "brute force"-heavy solution as a way to learn these new concepts and techniques.
When ingesting from a report, I also pull additional attributes from excel tables. I'm beginning my re-write with those lookup tables. The first of which I'm submitting here.
Initial goals:
- Programming to Interfaces not classes
- Making Services and Proxies rather than direct access to Excel sheets and ranges
- Using the PredeclaredId attribute to enable a Create method
- Thorough unit testing
Apart from general review, I also have some specific questions, which I'll post following the code.
Code
IAssetTableProxy
-- abstracts reference to the "physical" excel table's data rows
'@Folder("Services.Interfaces")
Option Explicit
Public Function GetAssetTableData() As Variant()
End Function
AssetTableProxy
-- Implementation
'@Folder("Services.Proxies")
Option Explicit
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim tblName As String
tblName = "AssetInfoTable"
IAssetTableProxy_GetAssetTableData = Worksheets(Range(tblName).Parent.Name).ListObjects(tblName).DataBodyRange.value
End Function
AssetInfo
-- a class to handle the three values for each row: Desc, Ticker, Type
'@PredeclaredId
'@Folder("Services")
Option Explicit
Private Type TAssetInfo
Desc As String
Ticker As String
AssetType As String
End Type
Private this As TAssetInfo
Public Property Get Desc() As String
Desc = this.Desc
End Property
Friend Property Let Desc(ByVal value As String)
this.Desc = value
End Property
Public Property Get Ticker() As String
Ticker = this.Ticker
End Property
Friend Property Let Ticker(ByVal value As String)
this.Ticker = value
End Property
Public Property Get AssetType() As String
AssetType = this.AssetType
End Property
Friend Property Let AssetType(ByVal value As String)
this.AssetType = value
End Property
Public Property Get Self() As AssetInfo
Set Self = Me
End Property
Public Function Create(ByVal theDesc As String, ByVal theTicker As String, ByVal theAssetType As String) As AssetInfo
With New AssetInfo
.Desc = theDesc
.Ticker = theTicker
.AssetType = theAssetType
Set Create = .Self
End With
End Function
IAssetInfoService
-- holds a collection of AssetInfo objects and
provides the needed lookups to data from AssetTableProxy
'@Folder("Services.Interfaces")
Option Explicit
Public Function Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
End Function
Public Function GetAssetTypeForDesc(ByVal Desc As String) As String
End Function
Public Function GetTickerForDesc(ByVal Desc As String) As String
End Function
AssetInfoService
-- implementation
'@PredeclaredId
'@Folder("Services")
Option Explicit
Option Base 1
Implements IAssetInfoService
Private Type TAssetsTable
AssetColl As Collection
End Type
Private this As TAssetsTable
Friend Property Get Assets() As Collection
Set Assets = this.AssetColl
End Property
Friend Property Set Assets(ByRef coll As Collection)
Set this.AssetColl = coll
End Property
Public Property Get Self() As IAssetInfoService
Set Self = Me
End Property
Public Function IAssetInfoService_Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
Dim twoDArr() As Variant
twoDArr = assetTbl.GetAssetTableData
With New AssetInfoService
Dim tempAsset As AssetInfo
Dim tempColl As Collection
Set tempColl = New Collection
Dim rw As Long
For rw = 1 To UBound(twoDArr, 1)
Set tempAsset = AssetInfo.Create(twoDArr(rw, 1), twoDArr(rw, 2), twoDArr(rw, 3))
tempColl.Add tempAsset, key:=tempAsset.Desc
Next rw
Set .Assets = tempColl
Set IAssetInfoService_Create = .Self
End With
End Function
Public Function IAssetInfoService_GetAssetTypeForDesc(ByVal Desc As String) As String
Dim tempTp As String
If Exists(this.AssetColl, Desc) Then
tempTp = this.AssetColl(Desc).AssetType
Else
tempTp = "Unknown Asset"
End If
IAssetInfoService_GetAssetTypeForDesc = tempTp
End Function
Public Function IAssetInfoService_GetTickerForDesc(ByVal Desc As String) As String
Dim tempTicker As String
If Exists(this.AssetColl, Desc) Then
tempTicker = this.AssetColl(Desc).Ticker
Else
tempTicker = "Unknown Asset"
End If
IAssetInfoService_GetTickerForDesc = tempTicker
End Function
Private Function Exists(ByRef coll As Collection, ByRef key As String) As Boolean
On Error GoTo ErrHandler
coll.Item key
Exists = True
ErrHandler:
End Function
Unit Testing
AssetTableTestProxy
-- proxy implementation for testing w/o dependency on actual excel table
'@Folder("Services.Proxies")
Option Explicit
Option Base 1
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim twoDArr(1 To 3, 1 To 3) As Variant
twoDArr(1, 1) = "Asset1"
twoDArr(1, 2) = "Tick1"
twoDArr(1, 3) = "Type1"
twoDArr(2, 1) = "Asset2"
twoDArr(2, 2) = "Tick2"
twoDArr(2, 3) = "Type2"
twoDArr(3, 1) = "Asset3"
twoDArr(3, 2) = "Tick3"
twoDArr(3, 3) = "Type3"
IAssetTableProxy_GetAssetTableData = twoDArr
End Function
TestAssetInfoService
-- Unit tests for Asset Info Service
Option Explicit
Option Private Module
'@TestModule
'@Folder("Tests")
Private Assert As Object
Private Fakes As Object
Private assetTbl As IAssetTableProxy
'@ModuleInitialize
Public Sub ModuleInitialize()
'this method runs once per module.
Set Assert = CreateObject("Rubberduck.AssertClass")
Set Fakes = CreateObject("Rubberduck.FakesProvider")
Set assetTbl = New AssetTableTestProxy
End Sub
'@ModuleCleanup
Public Sub ModuleCleanup()
'this method runs once per module.
Set Assert = Nothing
Set Fakes = Nothing
Set assetTbl = Nothing
End Sub
'@TestInitialize
Public Sub TestInitialize()
'this method runs before every test in the module.
End Sub
'@TestCleanup
Public Sub TestCleanup()
'this method runs after every test in the module.
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetTicker()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tick As String
tick = tbl.GetTickerForDesc("Asset2")
'Assert:
Assert.AreEqual "Tick2", tick, "Tick was: " & tick
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetAssetType()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim assetTp As String
assetTp = tbl.GetAssetTypeForDesc("Asset2")
'Assert:
Assert.AreEqual "Type2", assetTp, "AssetTp was: " & assetTp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetNotInTable_GetUnknownAssetMsg()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tp As String
tp = tbl.GetAssetTypeForDesc("unsub")
'Assert:
Assert.AreEqual "Unknown Asset", tp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Module1
-- additional sub to play around with functions
Option Explicit
Sub TestAssetInfoTable()
Dim assetTbl As IAssetTableProxy
Dim testAssetTbl As AssetTableTestProxy
Set assetTbl = New AssetTableProxy
Set testAssetTbl = New AssetTableTestProxy
Dim assetSvc As IAssetInfoService
Dim testAssetSvc As IAssetInfoService
Set assetSvc = AssetInfoService.IAssetInfoService_Create(assetTbl)
Set testAssetSvc = AssetInfoService.IAssetInfoService_Create(testAssetTbl)
Dim tp As String
Dim tick As String
tp = assetSvc.GetAssetTypeForDesc("AMAZON COM INC (AMZN)")
tick = assetSvc.GetTickerForDesc("AMAZON COM INC (AMZN)")
MsgBox ("Real Svc: tp=" & tp & "; tick=" & tick)
tp = testAssetSvc.GetAssetTypeForDesc("Asset3")
tick = testAssetSvc.GetTickerForDesc("Asset3")
MsgBox ("Test Svc: tp=" & tp & "; tick=" & tick)
End Sub
Specific questions:
- I initially had the "proxy" logic in the service class. But it felt like I was duplicating too many functions when I created the
AssetInfoTestService
class. Breaking it out toAssetTableProxy
andAssetTableTestProxy
allowed me to keep only one version of the service functions. But is this carrying things (abstraction?) too far?
Learning about interfaces, I believe I understand the following pieces:
- the contract created by each Function mentioned in the interface;
- the requisite coding of corresponding Interface_Function in the implementing class;
- the dimm-ing of class var "as Interface"; and
- accessing the functions with
classVar.Function
- However there seems to be an exception here. In
TestAssetInfoTable
I dim assetSvc asIAssetInfoService
. That interface has a Create function, and in the concrete class, I haveIAssetInfoService_Create
defined. But when I try to callAssetInfoService.Create(…)
I get a compile error that only clears when I change toAssetInfoService.IAssetInfoService_Create
. What am I missing there?
- However there seems to be an exception here. In
I see the "Option Base 1" thing. Since leave C pointers long ago, I haven't really had a religious belief one way or the other on 0- vs 1-based arrays. I went with it here, because when I began playing with the (extremely handy)
multiDimArray = Range
I noted the returned arrays are 1-based. And I kept screwing myself up between coding for those, and coding for my own 0-based ones. So I just opted to go all 1-based. Rubberduck Code Inspections do always throw that decision back in my face though, so let me ask here: are compelling reasons to not do that, or work arounds/tips for themultiDimArray = Range
1-based thing?
vba excel rubberduck
New contributor
$endgroup$
Background
I have a vba solution that I use to ingest investment text reports and reformat them for analysis in Excel. It works, but the macros involve a lot of direct manipulation of Excel objects, and have no unit-testing.
After finding RubberDuck, and reading several years' worth of excellent posts from @MathieuGuindon, I've decided to re-write the "brute force"-heavy solution as a way to learn these new concepts and techniques.
When ingesting from a report, I also pull additional attributes from excel tables. I'm beginning my re-write with those lookup tables. The first of which I'm submitting here.
Initial goals:
- Programming to Interfaces not classes
- Making Services and Proxies rather than direct access to Excel sheets and ranges
- Using the PredeclaredId attribute to enable a Create method
- Thorough unit testing
Apart from general review, I also have some specific questions, which I'll post following the code.
Code
IAssetTableProxy
-- abstracts reference to the "physical" excel table's data rows
'@Folder("Services.Interfaces")
Option Explicit
Public Function GetAssetTableData() As Variant()
End Function
AssetTableProxy
-- Implementation
'@Folder("Services.Proxies")
Option Explicit
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim tblName As String
tblName = "AssetInfoTable"
IAssetTableProxy_GetAssetTableData = Worksheets(Range(tblName).Parent.Name).ListObjects(tblName).DataBodyRange.value
End Function
AssetInfo
-- a class to handle the three values for each row: Desc, Ticker, Type
'@PredeclaredId
'@Folder("Services")
Option Explicit
Private Type TAssetInfo
Desc As String
Ticker As String
AssetType As String
End Type
Private this As TAssetInfo
Public Property Get Desc() As String
Desc = this.Desc
End Property
Friend Property Let Desc(ByVal value As String)
this.Desc = value
End Property
Public Property Get Ticker() As String
Ticker = this.Ticker
End Property
Friend Property Let Ticker(ByVal value As String)
this.Ticker = value
End Property
Public Property Get AssetType() As String
AssetType = this.AssetType
End Property
Friend Property Let AssetType(ByVal value As String)
this.AssetType = value
End Property
Public Property Get Self() As AssetInfo
Set Self = Me
End Property
Public Function Create(ByVal theDesc As String, ByVal theTicker As String, ByVal theAssetType As String) As AssetInfo
With New AssetInfo
.Desc = theDesc
.Ticker = theTicker
.AssetType = theAssetType
Set Create = .Self
End With
End Function
IAssetInfoService
-- holds a collection of AssetInfo objects and
provides the needed lookups to data from AssetTableProxy
'@Folder("Services.Interfaces")
Option Explicit
Public Function Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
End Function
Public Function GetAssetTypeForDesc(ByVal Desc As String) As String
End Function
Public Function GetTickerForDesc(ByVal Desc As String) As String
End Function
AssetInfoService
-- implementation
'@PredeclaredId
'@Folder("Services")
Option Explicit
Option Base 1
Implements IAssetInfoService
Private Type TAssetsTable
AssetColl As Collection
End Type
Private this As TAssetsTable
Friend Property Get Assets() As Collection
Set Assets = this.AssetColl
End Property
Friend Property Set Assets(ByRef coll As Collection)
Set this.AssetColl = coll
End Property
Public Property Get Self() As IAssetInfoService
Set Self = Me
End Property
Public Function IAssetInfoService_Create(ByRef assetTbl As IAssetTableProxy) As IAssetInfoService
Dim twoDArr() As Variant
twoDArr = assetTbl.GetAssetTableData
With New AssetInfoService
Dim tempAsset As AssetInfo
Dim tempColl As Collection
Set tempColl = New Collection
Dim rw As Long
For rw = 1 To UBound(twoDArr, 1)
Set tempAsset = AssetInfo.Create(twoDArr(rw, 1), twoDArr(rw, 2), twoDArr(rw, 3))
tempColl.Add tempAsset, key:=tempAsset.Desc
Next rw
Set .Assets = tempColl
Set IAssetInfoService_Create = .Self
End With
End Function
Public Function IAssetInfoService_GetAssetTypeForDesc(ByVal Desc As String) As String
Dim tempTp As String
If Exists(this.AssetColl, Desc) Then
tempTp = this.AssetColl(Desc).AssetType
Else
tempTp = "Unknown Asset"
End If
IAssetInfoService_GetAssetTypeForDesc = tempTp
End Function
Public Function IAssetInfoService_GetTickerForDesc(ByVal Desc As String) As String
Dim tempTicker As String
If Exists(this.AssetColl, Desc) Then
tempTicker = this.AssetColl(Desc).Ticker
Else
tempTicker = "Unknown Asset"
End If
IAssetInfoService_GetTickerForDesc = tempTicker
End Function
Private Function Exists(ByRef coll As Collection, ByRef key As String) As Boolean
On Error GoTo ErrHandler
coll.Item key
Exists = True
ErrHandler:
End Function
Unit Testing
AssetTableTestProxy
-- proxy implementation for testing w/o dependency on actual excel table
'@Folder("Services.Proxies")
Option Explicit
Option Base 1
Implements IAssetTableProxy
Public Function IAssetTableProxy_GetAssetTableData() As Variant()
Dim twoDArr(1 To 3, 1 To 3) As Variant
twoDArr(1, 1) = "Asset1"
twoDArr(1, 2) = "Tick1"
twoDArr(1, 3) = "Type1"
twoDArr(2, 1) = "Asset2"
twoDArr(2, 2) = "Tick2"
twoDArr(2, 3) = "Type2"
twoDArr(3, 1) = "Asset3"
twoDArr(3, 2) = "Tick3"
twoDArr(3, 3) = "Type3"
IAssetTableProxy_GetAssetTableData = twoDArr
End Function
TestAssetInfoService
-- Unit tests for Asset Info Service
Option Explicit
Option Private Module
'@TestModule
'@Folder("Tests")
Private Assert As Object
Private Fakes As Object
Private assetTbl As IAssetTableProxy
'@ModuleInitialize
Public Sub ModuleInitialize()
'this method runs once per module.
Set Assert = CreateObject("Rubberduck.AssertClass")
Set Fakes = CreateObject("Rubberduck.FakesProvider")
Set assetTbl = New AssetTableTestProxy
End Sub
'@ModuleCleanup
Public Sub ModuleCleanup()
'this method runs once per module.
Set Assert = Nothing
Set Fakes = Nothing
Set assetTbl = Nothing
End Sub
'@TestInitialize
Public Sub TestInitialize()
'this method runs before every test in the module.
End Sub
'@TestCleanup
Public Sub TestCleanup()
'this method runs after every test in the module.
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetTicker()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tick As String
tick = tbl.GetTickerForDesc("Asset2")
'Assert:
Assert.AreEqual "Tick2", tick, "Tick was: " & tick
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetInTable_GetAssetType()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim assetTp As String
assetTp = tbl.GetAssetTypeForDesc("Asset2")
'Assert:
Assert.AreEqual "Type2", assetTp, "AssetTp was: " & assetTp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod
Public Sub GivenAssetNotInTable_GetUnknownAssetMsg()
On Error GoTo TestFail
'Arrange:
Dim tbl As IAssetInfoService
Set tbl = AssetInfoService.IAssetInfoService_Create(assetTbl)
'Act:
Dim tp As String
tp = tbl.GetAssetTypeForDesc("unsub")
'Assert:
Assert.AreEqual "Unknown Asset", tp
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Module1
-- additional sub to play around with functions
Option Explicit
Sub TestAssetInfoTable()
Dim assetTbl As IAssetTableProxy
Dim testAssetTbl As AssetTableTestProxy
Set assetTbl = New AssetTableProxy
Set testAssetTbl = New AssetTableTestProxy
Dim assetSvc As IAssetInfoService
Dim testAssetSvc As IAssetInfoService
Set assetSvc = AssetInfoService.IAssetInfoService_Create(assetTbl)
Set testAssetSvc = AssetInfoService.IAssetInfoService_Create(testAssetTbl)
Dim tp As String
Dim tick As String
tp = assetSvc.GetAssetTypeForDesc("AMAZON COM INC (AMZN)")
tick = assetSvc.GetTickerForDesc("AMAZON COM INC (AMZN)")
MsgBox ("Real Svc: tp=" & tp & "; tick=" & tick)
tp = testAssetSvc.GetAssetTypeForDesc("Asset3")
tick = testAssetSvc.GetTickerForDesc("Asset3")
MsgBox ("Test Svc: tp=" & tp & "; tick=" & tick)
End Sub
Specific questions:
- I initially had the "proxy" logic in the service class. But it felt like I was duplicating too many functions when I created the
AssetInfoTestService
class. Breaking it out toAssetTableProxy
andAssetTableTestProxy
allowed me to keep only one version of the service functions. But is this carrying things (abstraction?) too far?
Learning about interfaces, I believe I understand the following pieces:
- the contract created by each Function mentioned in the interface;
- the requisite coding of corresponding Interface_Function in the implementing class;
- the dimm-ing of class var "as Interface"; and
- accessing the functions with
classVar.Function
- However there seems to be an exception here. In
TestAssetInfoTable
I dim assetSvc asIAssetInfoService
. That interface has a Create function, and in the concrete class, I haveIAssetInfoService_Create
defined. But when I try to callAssetInfoService.Create(…)
I get a compile error that only clears when I change toAssetInfoService.IAssetInfoService_Create
. What am I missing there?
- However there seems to be an exception here. In
I see the "Option Base 1" thing. Since leave C pointers long ago, I haven't really had a religious belief one way or the other on 0- vs 1-based arrays. I went with it here, because when I began playing with the (extremely handy)
multiDimArray = Range
I noted the returned arrays are 1-based. And I kept screwing myself up between coding for those, and coding for my own 0-based ones. So I just opted to go all 1-based. Rubberduck Code Inspections do always throw that decision back in my face though, so let me ask here: are compelling reasons to not do that, or work arounds/tips for themultiDimArray = Range
1-based thing?
vba excel rubberduck
vba excel rubberduck
New contributor
New contributor
New contributor
asked 3 mins ago
jdapjdap
11
11
New contributor
New contributor
add a comment |
add a comment |
0
active
oldest
votes
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
jdap is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214629%2fabstracting-and-unit-testing-lookups-in-excel-table%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
0
active
oldest
votes
0
active
oldest
votes
active
oldest
votes
active
oldest
votes
jdap is a new contributor. Be nice, and check out our Code of Conduct.
jdap is a new contributor. Be nice, and check out our Code of Conduct.
jdap is a new contributor. Be nice, and check out our Code of Conduct.
jdap is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f214629%2fabstracting-and-unit-testing-lookups-in-excel-table%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown