OOPs I did it again

I wrote my first programs in various forms of Basic (including SuperSyntax on Prime minis, which while it didn't explicitly claim to be a member of the family had very Basic-ish structure and syntax), but my formative years programming-wise were spent in Visual Basic (a semi-object oriented language) and C++ (straight-up OO).  Mix in something like fifteen years of viewing applications through VBA object model glasses, and it's not surprising that when I designed a layer to insulate our tests from the details of our UI I filled it with a bunch of objects.

Typical test code looked like this (using Visio as an example throughout, since I can't talk about my app yet; also omitting verification):

Page testPage = this.Application.Documents["Test"].Pages[0];
Shape shapeOne = testPage.DrawRectangle(0, 0, 4, 2);
Shape shapeTwo = testPage.DrawRectangle(5, 2, 4, 2);
Shape shapeThree = testPage.DrawRectangle(2, 5, 6, 3);
testPage.Selection.Add(new Shape[] {shapeOne, shapeTwo});
testPage.Selection.Move(shapeOne.TopLeft, shapeThree.BottomRight);

Nothing exciting here, just normal OO code where factory methods create objects, those objects do things to themselves, and collections add to and provide access to the objects they contain.  After using this model for a few months, however, we've discovered several problems:

  • There's a lot of infrastructure here that exists only because the model demands it, not because a test actually needs it.  All those collections, for example.  For execution, most tests just care about a document, page, or shape, not the entire collections of these things.  (The collections do become important for verification; more on that in a bit.)  We really ran into this this when we tried to match our model for new features against the corresponding specs and found that this infrastructure not only didn't really fit anywhere but in fact wasn't demanded by anything in the spec.
  • There's nothing protecting us from attempting invalid actions.  Going through the UI, it's impossible to modify shapes (set their fill color, say) that aren't selected.  However, there's nothing in this model that prevents that.  We could of course define IReadOnly and IReadWrite interfaces for every type, and cast objects to one or the other interface as appropriate -- and in fact we had done something similar.  This gets complicated very quickly, though, as we now have to define three types for every object.  This is also an example of infrastructure existing to serve the model, not the test case.
  • We aren't really testing like the user.  Our ultimate goal is to write tests that do things the way the user does.  Our model should support this frame of mind, but this one doesn't.  The user doesn't see collections of objects that do things to themselves; rather, they see a page on which they can draw shapes, select shapes, and modify the shapes they've selected.  They don't see the shape as deleting itself but rather see the page (or even the application) deleting the set of selected shapes.
  • There's no way to tell where data is coming from:  does any particular getter talk to the application's object model, the UI, or...?.  We need getters for two reasons:  first, to verify actions, and second, to feed into a subsequent step in the test.  For verification we very much care what the source of the data is, but test cases generally don't care where their data comes from as long as it is correct.

Our solution has two parts:

  1. We stopped hiding the sources of data.  Verification now talks directly to object models, UI, and whatever other data sources it wishes.  Test cases that don't care where their data comes from talk to the application.
  2. We reorganized our insulation layer based on the user's view of our application.  No more objects and collections.  No more gets either.  Instead we have the minimum amount of organization necessary to organize the mutators -- which are all methods now that the gets are gone.  These methods take some data (the formatting to apply, for example), but other data is assumed (for example, all formatting changes implicitly apply to the selected shapes)  As I said, these methods are named and organized based on the user's view of the application.  This gives us code like:

    string shapeOne = Model.ElementCreation.CreateRectangle(0, 0, 4, 2);
    string shapeTwo = Model.ElementCreation.CreateRectangle(5, 2, 4, 2);
    string shapeThree = Model.ElementCreation.CreateRectangle(2, 5, 6, 3);
    Model.Shapes.Select(new string[] {shapeOne, shapeTwo});
    Model.Shapes.Move(new Point(0, 0), new Point(6, 3));

This simple example highlights the important points of our new model:

  • No objects are ever created.  In fact, factory methods (e.g., CreateRectangle) are the only methods that return any type of data, and they return PODs (Plain Old Datatypes) such as strings, not custom objects.
  • Method parameters and return values are PODs, not objects created by the model.  Factory methods return a string containing the ID of the created thing.
  • File paths are always full paths.  The application may not always require full paths, but that's a detail method implementations are free to handle as they wish.  Tests don't care.
  • No collections or unnecessary objects.  Methods on the Shapes (static) class implicitly manipulate (add to, remove from, apply to) the set of selected shapes in whatever the current page happens to be; methods on the (static) Documents class do the same with the set of open documents.
  • It's impossible to do something illegal -- there's no way to say "Set the fill on this shape that...oh...isn't actually selected", for example.  (Yes, it is very possible to say "Set the fill on the selection that...oh...is empty".  But that's a valid test case, and something we want to allow.)
  • No gets.  Execution is the name of the game.
  • The code bears an amazing resemblance to steps in a help topic:  "Open the document.vdx file, then draw three rectangles.  Select two of the rectangles..."
  • You can't tell from this sample, but the implementation is about one-third the size of the OO model.

We have a functional model now rather than an object model, a state of affairs some pundits would tell you is abhorrent and anathema to modern design.  You know what?  We don't care.  This model is simple to maintain, extremely discoverable, and exactly solves our problem.  What more can you ask from a model?

*** Comments, questions, feedback?   Want a fun job on a great team?  Send two coding samples and an explanation of why you chose them, and of course your resume, to me at michhu at microsoft dot com. I need testers, and my team needs a data binding developer, program managers, and a product manager.  Great coding skills required for all positions.

Comments (2)

  1. Hi there,

    I’ve really enjoyed reading your blog. When I write testing code I almost always end up writing some minimal infrastructure to support the testing code. To what extent do you ensure consistency across your unit tests – inside cranks? across cranks?

    How often do you retool?

  2. The Braidy Tester says:

    Our primary method for ensuring consistency across tests is a) code reviews that help people to gain b) familiarity with the way we do things.

    I’m more concerned about consistency throughout our automation stack, though, than I am across tests. We code review there as well, but also add semi-formal reviews throughout the design and development process.

    Like the good agilers we aim to be, we retool exactly as often as we need to. <g/> We haven’t needed to change direction very often. When I first joined this group I set a course that was rather different than the one the team had been on — certainly a big change for everyone. This refactoring of our insulation layer is the only other change-of-meangingful-size since. While the organization of our layer is changing radically, the code in the layer is just moving sideways to a new home mostly unchanged. So it’s more of a mediumish-sized change.

Skip to main content