Abstract Factory Pattern For Reusable Testing
| 2011/05/12 | Posted by CCAdmin under Software Testing, Tips, Tools |
About ten years ago, when I was still a relative rookie software engineer I got moved into the Software Reuse group (SRG) within our software department. It was cutting edge…at least most people considered it cutting edge at the time. When I say Software Reuse group, I use the term “group” very loosely. The “group” was me and a guy named Dave. I took the move as new challenge though, and I embraced it as a way to make a positive difference.
As it turns out, this was a great move for me. First, and foremost, working with Dave was a pleasure. I learned more things from Dave than I can count. He was quite experienced and taught me about Reusable Software concepts, Software Design Patterns (which we used plentifully), UML and gave me my first taste in Object-oriented programming. I owe Dave a lot for what he taught me, and he was a great mentor.
Our task was to go around the various software programs that were going on within the software department and try to find common component needs. We would talk to each of the groups and learn about what components they needed in their software and then look for overlap between programs. For example, if we found three or four programs that were all going to develop a logging application, we would work with those groups to find out what their specific logging requirements were, and then we would design a reusable logger that met all the requirements of the programs so they could all use the reusable logger. Yes, this was “design for reuse”, not “ad-hoc reuse”.
So one of the first assignments I got was to write a reusable serial communications driver. There were programs that each had different communication devices. Also note, that most were on embedded platforms, so not only were they different serial communication devices, but each was on a different operating system (or no operating system, where my driver would be compiled in with the embedded software).
As you can see, this was quite a challenging task to be given. I wasn’t sure where to start, but luckily Dave already had a plan. As he was teaching me UML, he explained to me the Abstract Factory design pattern1. The basic idea is very similar to that of an Interface. Essentially the abstract factory defines interface classes for the desired functionality. The interface classes are then initialized with a “concrete factory”, that is, they are initialized with the specific class needed upon instantiation.
So in our case (I’ll recreate a simplified mock-up example for this post), we started with the concept of a “Abstract Communication Factory”. This Communication Factory has four interfaces: Read, Write, Flush and Status. As mentioned earlier, each of these interfaces have multiple “concrete factories” for each platform that is supported. So in this example, we’ll assume three devices. So each interface has three concrete functions mapped to each interface. The Read interface has a “Dev1_Read”, “Dev2_Read”, “Dev3_Read”, and so forth.
Side Note: If you are wondering how we got past the “cross-platform” problem, we coded all of this in standard ANSI C language using function pointers for assigning concrete functions to the abstract interfaces. ANSI C was by far the most common supported language for any and all platforms we used, so we were sure to have compiler support on all platforms. By keeping it ANSI C, we wouldn’t run into quirks that may have been unique to one compiler but not supported on another.
As you can see, this was a pretty big task, especially for a rookie software engineer. With the number of devices and interface support needed, this took a few months to get through. Now I had not learned much about software testing at this point in my career. I had done some minor unit testing and ad-hoc scenario type testing along the way, but it was not thoroughly tested by any stretch of the term. I figured we’d test it when we integrated with the programs that were going to use them. Dave, says, “Ooooh, no. We have to test this thoroughly ourselves before we give it to the programs. We need to test each interface, each concrete function, on each platform, etc…” My jaw drops to the floor when I realize I’m only half done. I’d just spent a few months on this, and I would have to write as much code to test it. Ugh….now how do I start that task.
As I start giving it some thought….and start looking at the big picture (the UML class diagram of the abstract factory pattern) a light bulb goes off in my head. I have a sudden realization that the abstract factory pattern (that was so perfect for reusable software because we could make concrete factories for each platform) was also the perfect architecture for the test code. What do I have to test? Four interfaces, with each interface able to go down to a different concrete function. It’s almost exactly the same architecture as the code itself.
The test architecture starts with a “Communication Test Factory”, instead of a Communication Factory. Instead of the Read, Write, Flush, Status interfaces, we have the Test_Read, Test_Write, Test_Flush, Test_Status interfaces. Similarly, each of the interfaces has a concrete function to back it up for each device. The only slight difference is that I show multiple unit tests defined for each device specific concrete test function. So essentially, we can use an abstract factory to test an abstract factory.
Maybe a little pseudo code will help understanding of how this might work in software.
First the Abstract Communication Factory pseudo code.
// Abstract Factory Definition
START AbstractCommunicationFactory()
INTERFACE Read(params)
INTERFACE Write(params)
INTERFACE Flush(params)
INTERFACE Status(params)
END AbstractCommunicationFactory()
// Concrete Functions
Dev1_Read(params)
{
// Code for Device 1 Read
}
Dev1_Write(params)
{
// Code for Device 1 Write
}
Dev1_Flush(params)
{
// Code for Device 1 Flush
}
Dev1_Status(params)
{
// Code for Device 1 Status
}
….Same for Dev2 and Dev3 concrete functions
START MAIN PROGRAM()
// Assign Concrete functions to interfaces based on startup condition
IF DEFINED DEV1
INTERFACE Read(params) = *Dev1_Read(params)
INTERFACE Write(params) = *Dev1_Write(params)
INTERFACE Flush(params) = *Dev1_Flush(params)
INTERFACE Status(params) = *Dev1_Status(params)
ELSE IF DEFINED DEV2
INTERFACE Read(params) = *Dev2_Read(params)
INTERFACE Write(params) = *Dev2_Write(params)
INTERFACE Flush(params) = *Dev2_Flush(params)
INTERFACE Status(params) = *Dev2_Status(params)
ELSE
INTERFACE Read(params) = *Dev3_Read(params)
INTERFACE Write(params) = *Dev3_Write(params)
INTERFACE Flush(params) = *Dev3_Flush(params)
INTERFACE Status(params) = *Dev3_Status(params)
ENDIF
END MAIN PROGRAM()
See….it’s not as complicated as it sounded. First you define the abstract factory interfaces. Then you must write the code for the concrete factory functions to implement the interfaces for each concrete factory you wish to implement. Lastly, within your main program you assign the interfaces to the concrete functions you desire based on startup conditions.
Then the Abstract Test Communication Factory pseudo code is very similar.
// Abstract Test Factory Definition
START AbstractTestCommunicationFactory()
INTERFACE Test_Read(params)
INTERFACE Test_Write(params)
INTERFACE Test_Flush(params)
INTERFACE Test_Status(params)
END AbstractTestCommunicationFactory()
// Concrete Test Functions
Test_Dev1_Read(params)
{
Run Dev1_Read_UT1()
Run Dev1_Read_UT2()
}
Test_Dev1_Write(params)
{
Run Dev1_Write_UT1()
Run Dev1_Write_UT2()
}
Test_Dev1_Flush(params)
{
Run Dev1_Flush_UT1()
Run Dev1_Flush_UT2()
}
Test_Dev1_Status(params)
{
Run Dev1_Status_UT1()
Run Dev1_Status_UT2()
}
….Same for Dev2 and Dev3 concrete functions
START MAIN TEST PROGRAM()
// Assign Concrete test functions to interfaces based on startup condition
IF DEFINED DEV1
INTERFACE Test_Read(params) = *Test_Dev1_Read(params)
INTERFACE Test_Write(params) = * Test_Dev1_Write(params)
INTERFACE Test_Flush(params) = * Test_Dev1_Flush(params)
INTERFACE Test_Status(params) = * Test_Dev1_Status(params)
ELSE IF DEFINED DEV2
INTERFACE Test_Read(params) = * Test_Dev2_Read(params)
INTERFACE Test_Write(params) = * Test_Dev2_Write(params)
INTERFACE Test_Flush(params) = * Test_Dev2_Flush(params)
INTERFACE Test_Status(params) = * Test_Dev2_Status(params)
ELSE
INTERFACE Test_Read(params) = * Test_Dev3_Read(params)
INTERFACE Test_Write(params) = * Test_Dev3_Write(params)
INTERFACE Test_Flush(params) = * Test_Dev3_Flush(params)
INTERFACE Test_Status(params) = * Test_Dev3_Status(params)
ENDIF
Run Test_Read(params) // This has test code for interface, AND it runs the concrete function
Run Test_Write(params) // This has test code for interface, AND it runs the concrete function
Run Test_Flush(params) // This has test code for interface, AND it runs the concrete function
Run Test_Status(params) // This has test code for interface, AND it runs the concrete function
END MAIN TEST PROGRAM()
This pseudo code for the abstract test communication factory is almost the same as the previous abstract communication factory pseudo code. First you define the abstract test factory with its test interfaces. Then you define the concrete test functions. Again, when you start the main test program you assign the concrete test functions to the test interfaces based on startup conditions.
This time, the main program shows that you run the test interfaces in the main test program. Running these interface tests will call the appropriate concrete test function. Running the concrete test function will also run the unit tests because they are called within the concrete test function.
Conclusion
Reusable software is a fantastic way to improve software development cycle time because a component used by many different software programs can be developed only one time. There is an overhead when developing robustly and designing “for reuse”, but the upstream overhead is heavily made up for on downstream savings. The abstract factory pattern is a design pattern that is very helpful when designing reusable software in this way.
Once software is developed, it must be tested. Software that is designed using the abstract factory design pattern can also be tested with test code that is also architected as an abstract factory. This allows us to have reusable testing for our reusable components. We “double” our savings by not only getting them on the development side, but also getting the same type of savings on the test side.
1 Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, Vlissides)





Recent Comments