Many-to-many checkboxes revisited, part 8: The persister class unit test

by Unknown user

In the previous installment I wrote a successful unit test that saved and loaded checkbox data. By way of refresher, here's that code:

SaveAndLoadData PROCEDURE  (*long addr)              ! Declare Procedure
 
 
ManytoMany                  CML_Data_ManyToManyLinks 
                            itemize(),pre()
RightRecordX                    equate
RightRecordY                    equate
RightRecordZ                    equate
                            end
Persister                   CML_Data_ManyToManyLinksPersisterForTPS
Filename                    cstring(500)
 
    CODE
    addr = address(UnitTestResult)
    BeginUnitTest('SaveAndLoadData')
     
    Persister.SetFilename(Filename)
    ManytoMany.Persister &= Persister
    ManyToMany.LeftRecordID = 1
     
    ManyToMany.SetLinkTo(RightRecordX)
    ManyToMany.SetLinkTo(RightRecordZ)
    AssertThat(ManyToMany.IsLinkedTo(RightRecordX),IsEqualTo(true), 'before save, record X should be linked')
    AssertThat(ManyToMany.IsLinkedTo(RightRecordY),IsEqualTo(false),'before save, record Y should not be linked')
    AssertThat(ManyToMany.IsLinkedTo(RightRecordZ),IsEqualTo(true), 'before save, record Z should be linked')
     
    ManyToMany.Save()
    ManyToMany.Reset()
    AssertThat(ManyToMany.IsLinkedTo(RightRecordX),IsEqualTo(false),'after reset, record X should not be linked')
    AssertThat(ManyToMany.IsLinkedTo(RightRecordY),IsEqualTo(false),'after reset, record Y should not be linked')
    AssertThat(ManyToMany.IsLinkedTo(RightRecordZ),IsEqualTo(false),'after reset, record Z should not be linked')
     
    ManytoMany.Load()
     
    AssertThat(ManyToMany.IsLinkedTo(RightRecordX),IsEqualTo(true), 'after load, record X should be linked')
    AssertThat(ManyToMany.IsLinkedTo(RightRecordY),IsEqualTo(false),'after load, record Y should not be linked')
    AssertThat(ManyToMany.IsLinkedTo(RightRecordZ),IsEqualTo(true), 'after load, record Z should be linked')

You can ignore the first two lines of code as they're standard setup for the unit test procedure, and required by ClarionTest. 

First I assign the Persister object to the ManyToMany object. 

Then I set some links to the right-side records and verify that the ManyToMany object correctly maintains those links. 

I call ManyToMany.Save(), which as you'll recall from last time checks to see if there's a Persister object, and if there is one it calls that object's Save method. 

I then call ManyToMany.Reset() and verify that all the previous associations have been wiped out, so I can be sure that anything I test after a Load() is there because of Load(). 

And finally I Load() the saved values and verify that ManyToMany now has the expected state.

Now let's take a closer look at this line of code:

ManytoMany.Persister &= Persister

In the ManyToMany class, the Persister property is declared like this:

Persister &CML_Data_ManyToManyLinksPersister

But in my code above, the Persister object isn't of type CML_Data_ManyToManyLinksPersister, it's of type CML_Data_ManyToManyLinksPersisterForTPS. How is it possible to declare a reference variable of one type and then pass it an object of another type? 

The answer is in the declaration for CML_Data_ManyToManyLinksPersisterForTPS:

CML_Data_ManyToManyLinksPersisterForTPS         Class(CML_Data_ManyToManyLinksPersister),Type,Module('CML_Data_ManyToManyLinksPersisterForTPS.CLW'),Link('CML_Data_ManyToManyLinksPersisterForTPS.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_)
Filename                                            cstring(500),protected
Construct                                           Procedure()
Destruct                                            Procedure()
Load                                                procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),derived
Save                                                procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),derived
SetFilename                                         procedure(string filename)
                                                End

The Class declaration

Class(CML_Data_ManyToManyLinksPersister)

means that CML_Data_ManyToManyLinksPersisterForTPS is derived from CML_Data_ManyToManyLinksPersister, so for all intents and purposes the ManyToMany instance sees the Persister object as an instance of CML_Data_ManyToManyLinksPersister. In fact, when the ManyToMany instance calls the Save() method, it "thinks" (if you'll forgive the anthropomorphism) that it's calling Save() on an instance of CML_Data_ManyToManyLinksPersister. 

But in CML_Data_ManyToManyLinksPersister the Save method is just a stub:

 

CML_Data_ManyToManyLinksPersister.Save          procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData)
    code

The code I really want to execute is in the CML_Data_ManyToManyLinksPersisterForTPS class:

CML_Data_ManyToManyLinksPersisterForTPS.Save    procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData)
x                                                   long
    code
    dbg.write('CML_Data_ManyToManyLinksPersisterForTPS.Save')
    ! Warning - the following code, although workable, is highly inefficient and will be rewritten
    if self.OpenDataFile()
        ! Clear out any existing 
        clear(Links:Record)
        Links:LeftRecordID = LeftRecordID
        set(Links:kLeftRight,Links:kLeftRight)
        loop
            next(LinksDataFile)
            if errorcode() or Links:LeftRecordID <> LeftRecordID then break.
            dbg.write('deleting existing LinksDataFile record with Links:LeftRecordID ' & Links:LeftRecordID & ', Links:RightRecordID ' & Links:RightRecordID & ': ' & error())
            delete(LinksDataFile)
        end
        dbg.write('records(LinksData.LinksQ) ' & records(LinksData.LinksQ))
        loop x = 1 to records(LinksData.LinksQ)
            get(LinksData.LinksQ,x)
            clear(Links:Record)
            Links:LeftRecordID = LinksData.LinksQ.LeftRecordID
            Links:RightRecordID = LinksData.LinksQ.rightRecordID
            dbg.write('adding LinksDataFile record with Links:LeftRecordID ' & Links:LeftRecordID & ', Links:RightRecordID ' & Links:RightRecordID & ': ' & error())
            add(LinksDataFile)
            if errorcode() 
                dbg.write('Error adding record: ' & error())
            else
                dbg.write('Success adding record')
            end
        end
        self.CloseDataFile()
    end

Ordinarily, the only way I could get this code to execute would be to change the reference in CML_Data_ManyToManyLinks from

Persister                                           &CML_Data_ManyToManyLinksPersister

to

Persister                                           &CML_Data_ManyToManyLinksPersisterForTPS

That would work but it would have the very undesirable side effect of hard wiring in my TPS persister. If I decided that in one part of my application I wanted a TPS persister and in another I wanted, say, an XML persister, I'd have to make a full copy of my CML_Data_ManyToManyLinks class and adapt it for the XML persister. 

Wouldn't it be nice if there was a way to keep the generic reference to  CML_Data_ManyToManyLinksPersister yet be able to call the code in any derived class?

Happily there is exactly such a mechanism, and it is called the virtual method

Virtual methods

The Save and Load methods in CML_Data_ManyToManyLinksPersister both have a ,virtual attribute:

Load                                                procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),virtual
Save                                                procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),virtual

When the virtual attribute is present on any method in the class the compiler creates something called a virtual method table (VMT) for that class. 

If a derived class has a method with the same name and prototype as a virtual method in the parent class, and that method has the virtual attribute, then any time the parent method is called the runtime library consults the VMT to see if there is a derived method that should be called instead. 

Virtual methods are one of the most powerful and compelling features of object-oriented programming. They allow you to selectively replace code in a parent class with your own code. Among other things, they are the mechanism by which most embed code is inserted into ABC applications. 

Derived virtual methods

To guarantee that your virtual methods are called in place of the parent class methods they must have the same name and prototype as the parent class methods. It's easy to make a mistake in a derived class and have a subtly different method signature. This could be the result of a typo, or because you made a change to the parent method and forgot to change the derived class method. What you end up with then is a derived virtual method that isn't derived at all; instead it's a new "top level" method that just happens to have a virtual attribute, but which will never get called in place of the method in the parent class. 

To avoid this problem always use 

,derived

rather than

,virtual

when declaring virtual methods in the derived classes. That tells the compiler that it must find a matching virtual method in the parent class. If there's a mismatch you'll get a "DERIVED procedure does not match parent prototype" error and you'll know something needs fixing. 

Virtual methods are so incredibly useful that some developers argue that all methods should be virtual by default. In Java, for instance, this is true of all non-static methods. In C# (as in Clarion) you have to explicitly declare methods as virtual. 

A blunt instrument

The Save method as written is something of a blunt instrument; it simply wipes out any existing data then adds in the new data. It will come in for some refinement a little later in the process.

The Load method is a simple bit of code that loads values from the file into the data queue. 

Wiring the unit-testable class into the UI example program

Unit testing can be incredibly useful, but sometimes wiring unit-tested code into a working program reveals new issues that need to be addressed. I'll cover those next time.