0

The motivation for this question is to obtain a concrete example of what good, unit testable code would look like for modifying a document. By way of background I appreciate that classes are great for defining and validating things, like for example:

Whether or not class modules are suitable for modifying a document seems to 'depend' (see Mat's Mug answer here: In VBA should code that modifies document be avoided in a class module) And I'm unable to find many examples where unit testing is used for code that modifies a document (perhaps there is a good reason for this?).

At any rate, based on my limited understanding of things I think that, for a word Add-In that 'adds formatted tables to documents', a Class Module provides the basis for a reasonable approach to adding a formatted table to a document... (let me know if I'm wrong).

Although I've tagged VBA-Excel I really am more interested in MS Word examples (which are so lacking) so by way of a fairly trivial example for MS Word, lets say I have code that adds formatted tables to documents at specified ranges.

For the purpose of a example lets assume:

  • basic order of events is:
    • add default table to document
    • subsequently format it in accordance with an INI file
  • formatting specified for all tables is:
    • table border line color
    • table row1 shading color
  • there are several tables specified by the INI file
    • tbl1-Border=wdRed
    • tbl2-Border=wdGreen
    • tbl1-Shading=wdRed
    • tbl1-Shading=wdGreen

So my next questions are:

  • how many classes should I plan for?
    • 1 for adding and formatting tables
    • 1 for reading INI file data
  • what would the structure of each class module look like?
  • should I (can I) unit test code that:
    • modifies a document (add a table)?
    • reads INI files?

I'm not expecting anyone to provide actual working code; but pseudo code, general advise and perhaps a few specific pointers would be much appreciated.

NOTE: If this question is too broad I'd be happy enough to split into separate questions

Community
  • 1
  • 1
SlowLearner
  • 3,086
  • 24
  • 54
  • Yes, I guess I am.... but actually I was planning on proposing to add a tutorial to the RD wiki once I had a better grounding on the topic ;-) was already unsure about editing the Wiki yesterday :-/ I did consult the guidelines for this one (https://stackoverflow.com/help/dont-ask) and am leaning on this: Constructive subjective questions: *inspire answers that explain “why” and “how” That said, I'm happy enough to add something more concrete but I fear my botched attempt would distract people from the core question as the focus shifts to code quality... – SlowLearner Aug 07 '17 at 03:54
  • @Mat'sMug that ^^^ said... any thoughts on Unit Testing for code that modifies a document? Doable? Worth doing / Don't go there? – SlowLearner Aug 07 '17 at 03:59
  • Seems off-topic for SO. But, almost anything can be unit tested. Not sure I would agree with using classes to insert tables - not because it couldn't work, but because it seems like a lot of work for a questionable amount of benefit? – Vegard Aug 07 '17 at 07:47

1 Answers1

2

A worksheet (or a Word document) is nothing more than an object encapsulating state / data.

You could go out of your way and wrap the worksheet/document with an interface that your code would depend on (e.g. IWorksheet, or IDocument), but that would be a tremendous amount of effort, for indeed little to no benefit - the unit tests would have to be using a "fake" implementation of that interface, which would be responsible for storing the test data/state so that your tests can assert that the code you're testing is working as intended. Complete overkill.

Instead, write your code so that it's given a Worksheet instance (i.e. avoid working against ActiveWorkbook and/or ActiveSheet), and does whatever it needs to do with it. Split up responsibilities so that when you invoke a method you don't have 20,000 things to assert to ensure your code does what it's written to do - but that shouldn't be anything new or different from what you're already doing, right?

'@Description("Adds a table named [tableName] on [sheet]. Returns the created table.")
Public Function AddTable(ByVal sheet As Worksheet, ByVal tableName As String) As ListObject
    'TODO: implement
End Function

A test for such a method might look like this:

'@TestMethod
Public Sub AddsListObjectToSpecifiedWorksheet()

    'Arrange
    Dim sheet As Worksheet
    Set sheet = ThisWorkbook.Worksheets.Add

    Dim sut As MyAwesomeClass
    Set sut = New MyAwesomeClass

    Const tableName As String = "TestTable1"

    If sheet.ListObjects.Count <> 0 Then _
        Assert.Inconclusive "Sheet already has a table."

    'Act
    sut.AddTable sheet, tableName

    'Assert
    Assert.IsTrue sheet.ListObjects.Count = 1, "Table was not added."
    sheet.Delete

End Sub

The sheet setup & cleanup code could be moved to dedicated TestInitialize/TestCleanup methods in the test module, as every single test method in that test module is likely going to need a fresh worksheet to play with, because you'll want every test to be independent and not share any state with the other tests.

Extracting the setup & cleanup code to dedicated methods in the test module removes fluff from the actual test methods. After all, a test module is a standard module that can have its own private fields and module-level constants:

'@TestMethod
Public Sub ReturnsListObjectReference()

    'Arrange
    Dim sut As MyAwesomeClass
    Set sut = New MyAwesomeClass

    If testSheet.ListObjects.Count <> 0 Then _
        Assert.Inconclusive "Sheet already has a table."

    'Act
    Dim result As ListObject
    Set result = sut.AddTable(testSheet, tableName)

    'Assert
    Assert.IsNotNothing result, "Table was not returned."
    Assert.AreSame result, testSheets.ListObjects(1), "Wrong table was returned."

End Sub

So you keep writing tests, each verifying one specific bit of behavior:

'@TestMethod
Public Sub TableNameIsAsSpecified()

    'Arrange
    Dim sut As MyAwesomeClass
    Set sut = New MyAwesomeClass

    If testSheet.ListObjects.Count <> 0 Then _
        Assert.Inconclusive "Sheet already has a table."

    'Act
    Dim result As ListObject
    Set result = sut.AddTable(testSheet, tableName)

    'Assert
    Assert.AreEqual tableName, result.Name, "Table name wasn't set."

End Sub

That way when you, future you, or whoever inherits your code looks at your test suite, they'll know exactly what your code is supposed to do, and by running the tests they'll know that your code does what it's intended to do.

Whether you want a test that breaks when the code is modified to make the tables have blue borders instead of green, is entirely up to you and your requirements.

In your specific case involving an INI file, IMO the "file" part is an implementation detail, and you wouldn't want a unit test to depend on some file somewhere on the network. Instead, you'll have a class or data structure to hold the configuration key/value pairs; the test's "arrange" part would be responsible for setting up the configuration data, and when you "act" you pass the configuration to your SUT and then assert that the resulting state matches the specified configuration.

The code that reads/writes the actual INI file would be another concern entirely, with its own test code, which would also avoid hitting the file system: you want to test your code, not whether the Scripting Runtime's FileSystemObject does its job.

Note that whether AddTable is a member of MyAwesomeClass or some utility standard procedural module makes absolutely no difference as far as testing is concerned; unit tests don't tell you how you regroup/abstract functionality and organize your code.


The latest version of Rubberduck (pre-release 2.1.x builds) include the beginning of a "fakes/stubs" framework that can be setup to intercept a number of specific standard library calls, by hooking into the VBA runtime itself. For example you don't want a unit test to pop a MsgBox, but if the method you're testing requires one, you can intercept the MsgBox call while the test is running (and even setup its return value, e.g. to simulate the user clicking on [Yes] or [No] or [Cancel]), but that's another topic entirely.

Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • Mat's Mug very comprehensive and thank you! Will definitely read this a few more times - I can see that I'm still not looking at this the right way, my 'chunks' are way way way to big - the notion of not accessing a file object to test how my code would respond to information in an INI file drives this point home nicely. The different roles of class and std module are also much clearer. Thank you for answering the questions that I didn't quite know how to ask :-) – SlowLearner Aug 07 '17 at 23:24