|
|
||||||||||||||||||||||||||||||||
William C. Wake's BlogTesting ArchivesExtreme Test MakeoverPosted by wwake on April 15, 2006 at 11:13 AM | Permalink | Comments (0)Brian Marick and I are co-hosting a session, "Extreme Test Makeover," at the Agile Conference, currently scheduled for Monday, July 24. The idea is that you bring your code and tests, ready to run. We'll have a number of people who are experts in unit testing and acceptance testing, to help you improve and extend your tests. Some people who are planning to help include Ward Cunningham, Ron Jeffries, Jim Shore, Janet Gregory, Charlie Poole, and others. Our ideal is that you bring production code and tests, so you can walk away with concrete, ready-to-go improvements. But you can bring whatever you want that's interesting enough to test. The sessions will be 90 minutes long, including five minutes up-front for experts to talk, and ten minutes at the end to record lessons learned. Some sessions will be intimate, you and the expert and perhaps a couple people looking over your shoulder. For others, we plan to have a projector and microphone so many others can look on. Brian has set up a yahoo group: test-makeover to let people connect with the artists. There will be a signup sheet at the conference too. Brief review - Fit for Developing SoftwarePosted by wwake on July 08, 2005 at 05:53 AM | Permalink | Comments (0)Fit for Developing Software, by Rick Mugridge and Ward Cunningham. [My bias disclosure - I know both Rick & Ward, I was a reviewer, and I've written for their publisher myself. This review is substantially as posted on the agile-testing group.] Fit (see http://fit.c2.com) is a testing framework that Ward Cunningham developed. A test author writes tests as tables in a document that can be converted to HTML (e.g., Word, Excel, text editor, etc.). The programmers develop fixtures that connect to the system under test. Fit mediates the tests and the fixtures to run the tests, and captures the results. It colors in the document using red/yellow/green to show what happened. The book is in two halves. The first half is targeted at test authors: from the user perspective, how does Fit work? It covers the basics of tables, fixtures, error handling, and so on. Then it goes into an extended example (several chapters) following a team developing rental software. The first half closes with advice about designing better tests. The second half is targeted at programmers. While programmers should really read and understand the first half of the book, test authors will probably at most skim this half. This half starts by explaining how to implement various types of fixtures. Then it continues the earlier rental software example by showing the fixture code that would be developed. Finally, this half closes with some advanced topics: Fit's architecture, custom fixtures and runners, and model- based test generation. The authors have done a good job explaining Fit from both the test- writing and programming perspectives. The text is clearly written, using plenty of examples, frequent breaks for Questions and Answers along the way, and exercises at many chapter ends. This book is unique. While you can find information about Fit and fixtures on the web, what's on the web is much less readable than what this book provides. The book also gives you an extended example and helpful advice from two experts. If you are considering Fit, or just want to understand its philosophy, this book provides the clearest explanation I've seen. For test authors, the first part of the book justifies the whole price. For programmers who need to understand how and why fixtures work, it's even more of a bargain.
Fit for Developing Software, by Rick Mugridge and Ward Cunningham,
with a foreword by Brian Marick. Prentice Hall, 2005. ISBN
0-321-26934-9.
Fit code, part 8 of 8 - RowFixturePosted by wwake on June 25, 2005 at 09:08 AM | Permalink | Comments (0)A RowFixture is used to test that a set of items is as expected. The fixture flags surplus or missing items. They look like this:
Data and Abstract MethodsFirst, I'll note that this fixture extends ColumnFixture. This lets it pick up bind() and check(). The former handles the "header" row; the latter makes sure execute() is called. But due to the way the overrides happen, that method is called under different circumstances than for ColumnFixture. I don't see anything that will call reset() on a per-row basis.The fixture holds three bits of data: an array containing the results of the query, a list of surplus items, and a list of missing items. From showing usages, I see that the list of surplus items is a list of domain objects; the missing list is a list of Parses. The first abstract method is query(). It is responsible for producing an array of the "actual" results. The second abstract method is getTargetClass(). It returns the class object representing the type of the row. It's abstract for an interesting reason: the parent class ColumnFixture defines that method to return the type of the fixture itself. That would just lead to weird errors. By making it abstract, it forces the user to override it. This is an interesting twist - usually my abstract methods are at the top of the hierarchy, and may get filled in along the way down. In this case, the method is becoming abstract in the middle. In a sense, that happens because RowFixture and ColumnFixture have a slightly strained relationship. Maybe I'm just not getting why the latter is an example of the former; it feels like the inheritance is more for implementation than anything. doRows() - The Overall AlgorithmThe main algorithm is in doRows(): bind the columns (ala ColumnFixture), run the query() to get a list of actuals, run a match(), then add rows for any surplus or missing items.Along the way, this method calls two overloaded list() methods: one for making a list of Parses, the other for making a list of objects. This parallel structure (methods for each of the two main data structures) continues across the class. Method doRows() calls buildRows() to add in new rows for the surplus values. This method works by building up a "fake" head of a parse chain, then adding each item to the last one in the list. In the end, it throws away the head and returns the interesting part of the list, which gets attached to the end of the table. This seems like a little pattern worth remembering if I ever need to add rows to a fixture. match() - The HeartThe match() routine is a recursive algorithm. Given the list of expected items, the list of computed items, and a column to start looking in, it figures out what matches and what's missing or surplus.Since this algorithm looks complicated, I'm going to start by just looking around. First, what are the places that call it? By doRows() certainly, since that's how we got here. Then it's called recursively at two places inside match() itself. The good news is we don't have some sort of pair of mutually recursive methods. Recursive algorithms have a base case and a recursion case. The recursive case here is just incrementing column, and passing along lists. The column is always incrementing, and the first if says that if we've exceeded the number of columns, we should just do a check on the lists. That makes it look like we'll always terminate: we either increment column, in which case we'll eventually stop, or we do something that doesn't recurse, which will stop as well. (Or rather, if it doesn't, it won't be because of the recursion here.) The other thing to look at on these recursive calls is the lists. We know the column gets bigger - do the lists get smaller? One case passes on the originals, so we know it's no bigger. The other is trickier - I see things that seem to indicate that the lists will shrink (tests for 0 or 1 item), but it's not obvious that it must be so without a little digging. So, from the top of match(): the first case we mentioned before - if we're past the number of columns, do a check() on the currents lists. (We'll come back to that method later.) The second case says that if the current column binding is null, move on to the next column. The third and final case is where the meat is: we're in a column in the middle, trying to match. So, we build up two maps: one for the expected, one for the computed ("actual"). Each map has a list of items that have the given value at the chosen column. We pull out the keys in either list, and work our way through them. Here, there are four interesting cases:
eSort() and cSort()There are two "sort" routines, one for eexpected and one for computed. They're pretty similar, so I'll describe them together. (I don't get why they're a sort of any sort, though.)Each routine produces a Map, from key values to a List of Objects (either domain objects or Parses). The bin() method takes care of putting items in the map. That method expands an Array into a List; the RowFixtureTest mentions a bug in that neighborhood and I suspect this is to address that. The sort() methods handle exceptions and rows with no value in a particular cell. Back to the AlgorithmI think I understand what's going on enough to put it into words now. To make a match, we start in a given column. Each list gets divided into buckets, based on the value of the cell at that column. If buckets have 0 or 1 item, then we have a good enough match. Otherwise, we'll look at more columns for those items. Eventually, we'll run out of items or columns.check()The last interesting bit is around the check() routine. It goes through and checks the columns one at a time, using the normal TypeAdapter facilities. The routine is recursive: it peels off the front of each list, recursing until one or both lists is empty.Leftovers and LearningsI had a question about whether it acted like a multi-set or a set. It looks like it's basically multi-set-like, from a simple test with a list of integers.The other big thing for me to wonder is how I'd have done a similar fixture. I think I'd have expected a simple set. The problem is, that's fine for the query() side, but not so good for the "expected" side: how would you get from those values to construct the objects to compare as sets? (Knowing the contents of an object's fields and return values from its methods doesn't tell you how to construct it.) Another alternative would be to get the query values, and match each one up against the rows. The naive algorithm for this is a little slow (n^2). It might be a bit simpler. I suspect its report wouldn't be as nice. The current algorithm is able to take advantage of partial matches - if enough data cells make it a unique match, it can then know it has the "right" element even though some of the fields/methods are wrong. Closing Out...That concludes my tour of Fit. I focused on the main fit package, skipping a couple more minor classes. The code reading was a good exercise for me - I have a better sense of some of the tradeoffs in the code, and of the dynamics in the Fit community.Fit code, part 7 - ColumnFixturePosted by wwake on June 24, 2005 at 12:28 PM | Permalink | Comments (0)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 RowMethod 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 BasicsDoRow() 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 HandlingThis 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. (Pause) 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
Fit code, part 6 - TypeAdapterPosted by wwake on June 14, 2005 at 07:38 PM | Permalink | Comments (0)C# FitI've gotten some mail letting me know that the C# Fit has forked a bit - there's a newer version that's the regular Fit distribution, and an older/modified version that's part of Fitnesse. I was having trouble extending the Fitnesse version. There's an effort to do some unification work this summer; that should help.TypeAdapterTypeAdapter exists to give a common interface to types, so they can all have setters, getters, and parsers. There are three factory methods, all named on(): one takes a fixture and a class, another a fixture and a field, and a third takes a fixture and a method.This gives the unification of fields and methods. In Fit, you can have a ColumnFixture with a field, and it has an implicit setter ("name") or getter ("name()"). Or you can have a method (also "name()"). For most purposes, we don't care what it is, we just want to treat it as a setter or getter. The TypeAdapter has five fields:
MethodsThe get() method tries to do a field access if the adapter is a field, or a method invocation if it is a method.The set() method does a field set. (Could we extend this to call "setter methods" with a signature like setFoo(Foo value)?) The invoke() method assumes that a method is set, and calls it with no parameters. The parse() method asks the fixture to parse the string according to type. In C#, parse() is something each type (even primitives) define. I'm sure that simplifies some of this code. So let's say we have a ColumnFixture where phone() is of type PhoneNumber. How do we make that get parsed naturally? It looks like it works its way back to Fixture, which has a parse() method. So the ColumnFixture overrides it, and checks for an attempt to parse a class it knows about. It seems like we could do some fanciness here, too, pushing an attempt to parse onto the domain classes. (So let Fixture.parse() take a look for "type.getMethod("parse")"; if we don't want that we could subclass and override to avoid it.) Primitives and Their ClassesThe rest of this file is a whole bunch of subclasses of TypeAdapter: one for each primitive type, and one for each corresponding Class. Most of these are the same: the primitive type's adapter is a subclass of the Class one. The primitive's defines set(), and the class one defines parse()The last one is the only exception: Arrays. There, the parse() method tokenizes it by looking for commas. Like so many other places in Fit, it trims spaces. Each element of the array is given its own TypeAdapter. The toString() method puts the commas in when printing it out. ReflectionThe big surprise here is the idea of unifying methods and fields. I'm not sure how I'd have come to the realization that they're the same at a level we care about. (That insight is of a piece with the whole framework - I've understood reflection for years, but I've used it more for plugin-style work than anything like Fit that uses test data to drive the reflection.)Fit code, part 5 - ActionFixturePosted by wwake on June 13, 2005 at 05:47 AM | Permalink | Comments (2)Wow - this one is a lot cleaner than I expected. I had tried overriding the C# version and had all kinds of grief. This version is straightforward and extensible. FieldsThe class has three fields:
Actor holds the object created by a start() action. Notice that it is static - that is what lets separate ActionFixture tables keep working with the same object without repeating start in every table. Empty is the easiest - it's just an empty list so that things that want a list of argument types can have one. I marked it final, since it's never changed. Methods: doCells()The first method is doCells(). It saves off the cells, so other methods have access to this row's Parse. Then it looks up the method in the first cell, and invokes it. (This method will be one of "start," "press," "enter," or "check.")The fixture invokes the method on itself by "getClass().getMethod()" - looking for the method on itself. This is a place where the Java version is nicer than the C# one. The C# version hard-coded that line to the equivalent of "ActionFixture.class.getMethod()". That meant that a subclass of ActionFixture would only have access to the four methods ("start" etc.) originally planned. The Java version lets you extend this vocabulary easily. Another thing to notice is that the fixture calls getMethod() on cells.text(), not camel(cells.text()). That's a pity - my extended vocabulary has to be spelled exactly. (I don't think the rules for camel casing are consistent throughout. I'm probably getting hung up from Fitnesse experience - I think it may have slightly different rules.) Methods: ActionsStart() is straightforward. It creates an object of the named type, and stashes it in the actor field. I note that it doesn't camel-case its argument, so "start MyFrameObject" is different from "start my frame object". (The latter won't work.)Enter() looks on the actor for a one-argument method it can use as a setter. It creates a TypeAdapter, which knows how to parse objects, passing it the cell text. Then it invokes the setter. Press() invokes the named 0-argument method on the actor. Check() assumes its 0-argument method is a getter, fetches the result, and passes it to Fixture's check() routine, which does the comparison and cell coloring. Methods: method()The two variants of method() try to find an n-argument method on the actor The simple form camel-cases, so the fields on the start object can have the more user-friendly form. ("start MyFrameObject // press the rightmost button".) It double-checks that there is only one possible method. (So, if "firstName(int)" and "firstName(String)" both exist, it will report that it doesn't know which to use.)
Next time, I'll take up TypeAdapter.
Fit code, part 4 - FixturePosted by wwake on June 03, 2005 at 02:02 PM | Permalink | Comments (0)Fixture: Fields and Two Helper ClassesThere's a Map summary that accumulates things like the "run date." I don't know why the top-level Fixture has this, but it does. The fixture fit.Summary walks through this table and gives summary statistics.There's a field counts that has counts of tests passed, failed, and exceptions/errors. The Counts class is just a data bag for these things. When a fixture calls wrong(), for example, the count is incremented. The last field is args, which has the arguments from the first row of the fixture. The method getArgs() returns a String[] and lets a fixture use them. I don't think this is in the C# version yet but we definitely use that sort of thing there. There's an internal class RunTime. It takes a snapshot of the current time. Right now, the only use of this is to put it in the summary, under the key "run elapsed time". Presumably some fixtures pull the RunTime object back out, and use toString() to display the elapsed time. But nothing in the standard distribution appears to use it directly. (Fit.Summary will display the elapsed time when it dumps the summary table.) Starting FixturesNow we come to doTables(), the top-level method. (It's called by FileRunner, passing in a Parse for each table.) This method first looks at the name in the first cell of the first row of the table. Then it tries to create the fixture, then use it via interpretTables(). Along the way I note that this routine is using a couple null checks; I wonder if those are necessary? If the first table's fixture fails to be created and run, it runs the remaining fixtures via interpretFollowingTables().Method getLinkedFixtureWithArgs() tries to load the fixture named in header.text(), then it sets up the arguments (for getArgs(). The method loadFixture() takes the name of the fixture, and attempts to "new up" the named fixture via reflection. Between the last method and this, I'm worried by what I don't see: what routine uses the camel method? That suggests a test: let's load "fit.ActionFixture", "fit.Action Fixture", and "fit.Action fixture" and see what happens. From what I understood going in, all three should be ok. From what I'm seeing here, I don't see what would make that work. Why did I expect this? Because ColumnFixture does it for column names. It turns out that's not a good enough reason. The test shows that only "fit.ActionFixture" loads. Up to interpretTables() again. It does getArgsForTable() - again. There's even a comment to that effect. I don't see why it should be necessary, though. Actually - it's all a little subtle, and I'd say the comment is misleading. The comment says, "// get them again for the new fixture object". But really, that's what we did in getLinkedFixtureWithArgs(). Now we're getting the arguments for the original fixture. It works like this: when FileRunner starts, it runs doTables() on a new Fixture object. That's the object that tries to pull fixtures from tables and run them. When the first table is seen, its arguments are pulled out and given to the corresponding fixture. But then they're also copied back to the initial fixture as well. I imagine they're actually rarely needed there. At any rate, interpretTables() then calls doTable(), which does a straightforward job of working its way into doCell(). Finally, it calls interpretFollowingTables(). InterpretFollowingTables()By the time we're here, we run through a loop, looking up fixtures and then interpreting them with doTable(). For these, we don't change the arguments on the fixture that started it all. Why not? I can only guess it has to do something with the way DoFixture wants to work - treating the first table special.All this work seems a little off - it seems the Fixture class is paying for interpretation that a particular table wants. I'm a long way off from looking at DoFixture, but if that's the table that should be first, it seems to me like it should pay for this complexity. I know I'm second-guessing here... Check()The other routines are either straightforward, or I've looked at them already. The exception is a largish routine at the bottom: check(). This is a helper method, used by some subclasses. It deals with blank cells, null adapters, "error" expected (to deal with expected exceptions), and text that should match. In each case, this method puts the output in the cell, colored appropriately.Up next...I think I want to look into ActionFixture next. I had an unhappy session trying to extend the C# version (which appears to be older). Then I want to dig into how TypeAdapters work.Fit code, part 3 - Parse and FixturePosted by wwake on June 02, 2005 at 07:32 AM | Permalink | Comments (0)ParseFirst I want to chase down a couple oddities in what I saw last time. It boils down to these two tests:
// This test shows offset isn't applied the way I expected
public void testOffset() throws Exception {
int offsetToData = 2;
Parse p = new Parse(
"xx
// This test shows cells with embedded tables "go away"
public void testInnerTables() throws Exception {
Parse p1 = new Parse(
"
FixtureTestIt won't take long to look at this: it only has one test!
assertEquals(" ", Fixture.escape(" "));
The method basically handles converting plain text to have HTML entities.
But there's a little more going on in Fixture... FixtureI've only got a few minutes, so this is a quick overview. From the FileRunner, I can see that doTables() is an important entry point. Last time I looked in the Java version, it was simpler than it is now. There are comments showing code added for DoFixture and for fitnesse, and they've made it all a little trickier. This is all to make an improvement at the user level.The core of this is: doTables() calls getLinkedFixtureWithArgs() and interpretTables(), which (eventually) calls doTable(), which calls doRows(), which calls doRow() once for each row, which calls doCells(), which calls doCell() once for each cell. In Fixture, doCell() calls ignore(), which marks the cell gray. ColumnFixture(), for example, overrides doCell() to do something interesting (like look for expected results). I'm going to skip over how the first table gets loaded and interpreted - it looks interesting (i.e., tricky:) Instead, I'll peek down to a section labeled "Annotation". This area contains methods that can mark and color cells: right(), wrong(), info(), ignore(), error(), and exception(). I've seen these called in several of the standard fixtures before. I see where exception() puts the stack trace into the cell. That gets SO ugly when something goes wrong. (It makes the cell huge, full of scary content useful only to programmers.) Maybe someday I'll take a whack at a more readable version. UtilitiesThe final section is Utilities. The lightly tested escape() method is there. There's also a method to put words into camel case. I'll add a test:
assertEquals("twoWords", Fixture.camel("two words"));
assertEquals("MiXedCAsE", Fixture.camel("MiXed cAsE"));
assertEquals("aFewWordsTogether", Fixture.camel("aFewWordsTogether"));
assertEquals(
"acronymsLikeHTMLStillUppercase",
Fixture.camel("acronyms like HTML still uppercase"));
Hmm. This is perhaps not the pattern I'd have chosen. I have the impression some of the other languages do it differently.
There's a parse() method that appears to handle only Strings, Dates, and ScientificDoubles. I know that C# works a little differently, since parse() is more integrated. There's a check() method that looks too complicated to understand in 30 seconds. It uses a TypeAdapter, which is another class I want to look at soon. Finally, there's a method to get the arguments from a Fixture. This is new - it used to be that the first row had the fixture only. (Rather, you had to parse any arguments out yourself.) Now that's built in, accessed via getArgs(), which returns a String array.
That's it for today. Next time, I want to dig into how fixtures get started (since this has changed some), and into the check() method.
| ||||||||||||||||||||||||||||||||
|
|