Fit code, part 7 - ColumnFixture
ColumnFixture is an easy fixture to understand from the user's point of view: each row is a test case, with some columns being inputs, and others being outputs:
doRows() - Capture the Header Row
Method doRows() calls bind() to peel off the header row, then processes the rest of the table. Bind() creates an array of TypeAdapters, one per column in the table. If the column header cell is empty, the ColumnBinding is set to to null. (An opportunity for a Null Object? Later, we check for null.)
If the cell ends in "()", it's a method call, and set via bindMethod(). Peeking inside there, it camel cases the name (so "shirt size()" becomes "shirtSize()") and uses the TypeAdapter.on() method to create the adapter.
Otherwise, the cell is assumed to be a field name, set via bindField(). That helper method also camel-cases the name and uses the "field" version of TypeAdapter.on().
Any problem in the header parsing marks the cell with an exception.
doRow() - Handling the Basics
DoRow() is fairly simple. It calls a stubbed-out reset() method before handling anything in the row. This lets a fixture reset anything on a per-test basis. It lets the normal row-processing take place, then it checks if execute() has been called; if not, it calls it.
Execute() is to be called before processing the first column that represents a method name: you can have a bunch of inputs, use execute() to make things happen, then check the outputs. If you don't override execute(), then you either have to know which column is first and make it kick things off (which is brittle), or you let each output column compute "from scratch". (Reading it here makes me wonder if I've been diligent about this in all my ColumnFixtures:)
If there are any unhandled exceptions, they're attached to the first leaf cell of the table.
doCell() - Per-Cell Handling
This boils down to four cases: empty text, null TypeAdapter (mentioned earlier), field, and method.
For empty text, we call execute() and move to the superclass' handling of the cell, which marks the cell as "info".
This seems a little odd to me - why should a missing value trigger that?
I'm going to write a test for it, but it took a couple minutes to figure out how to do so. I think what I'll do is create a table with a default value for x and y, leave x blank, and have execute() print the value of y. If it's called when x is processed, it'll print the default value rather than the one that was set.
OK - I'm back, and it does act like I expected from the code - execute() is called for a blank cell.
I guess that makes sense to do if the blank cell were a call cell (like plus()); I'm not clear on the value for an input cell.
Back to doCell(): if the TypeAdapter is null, it ignores the cell. If it's a field, it parses the text and sets the field. If it's a method, it calls check().
check() - Calling execute()
Method calls go through the (overridden) check() routine. This is really here to make sure the execute() method gets called if it hasn't yet. Then it just defers to the superclass version, which calls the method and compares the result.
What I've learned
- Don't forget about reset() and execute()
- Execute() is a little word in the face of blank input cells.