Many-to-many checkboxes revisited, part 6: Managing the link data
by Unknown user
Having dealt with most of the messy UI code, I'm ready for some clean, efficient, highly rational business logic. This is work I usually do in unit tests. And the first thing I need is a unit test app, one that will run with my ClarionTest unit testing framework.Â
I begin by creating a new unit test app from a TXA that comes with ClarionTest.Â
By convention I usually create a unit test app for each class, and I give the app the same name as the class with a "Tests" suffix. I know my class prefix will be CML_ (for ClarionMagLibrary). But I haven't got a class name yet.
Which leaves me stumped again:
Coming up with a name is hard because the whole time I'm thinking about class design I'm trying to keep the class as generic as possible. A class that handles just one situation is a last resort.
While I'll be using this class in conjunction with a UI class, it doesn't have anything to do with the UI. But it does have to do with data, and it does have to do with managing many-to-many relationships. So provisionally I've decided to call the class CML_Data_ManyToManyLinks and the app CML_Data_ManyToManyLinksTests.
I create the class using John Hickey's excellent ClarionLive! Class Creator, part of the ClarionLive! Utilities:
My baseline class uses the CML conventions, and is included in CML as CML_BaseClass.inc and CML_BaseClass.clw.
The first unit test
The TXA created a sample unit test procedure for me. But what will my first unit test be?Â
At the risk of repeating myself even more than usual, one of the things I like best about test-driven development is it forces me to think about how I want to use my classes long before I think about how to write them. And that's almost always a more productive approach.Â
I see CML_Data_ManyToManyLinks as my repository of information about whether any two records from each of two files linked. As with the original class that is the inspiration for this series, I'm going to use the arbitrary terms "left" and "right" to define those two sets of records.
Let's say I have "left" file records A, B and C, and "right" file records X, Y and Z.Â
A is linked to X and Z, and C is linked to Y and Z. B is linked to none of the right records.Â
I'll need a way to store the links in CML_Data_ManyToManyLinks and a way to query CML_Data_ManyToManyLinks to find out if a link exists.
Now I can start writing some code. Here's my first test procedure's data section:
ManytoMany CML_Data_ManyToManyLinks itemize(),pre() LeftRecordA equate LeftRecordB equate LeftRecordC equate RightRecordX equate RightRecordY equate RightRecordZ equate end
And here's the code:
ManyToMany.AddLink(LeftRecordA,RightRecordX) ManyToMany.AddLink(LeftRecordA,RightRecordZ) ManyToMany.AddLink(LeftRecordC,RightRecordY) ManyToMany.AddLink(LeftRecordC,RightRecordZ) AssertThat(ManyToMany.HasLink(LeftRecordA,RightRecordX),IsEqualTo(true), 'Test 1 failed') AssertThat(ManyToMany.HasLink(LeftRecordA,RightRecordY),IsEqualTo(false),'Test 2 failed') AssertThat(ManyToMany.HasLink(LeftRecordA,RightRecordZ),IsEqualTo(true), 'Test 3 failed') AssertThat(ManyToMany.HasLink(LeftRecordB,RightRecordX),IsEqualTo(false),'Test 4 failed') AssertThat(ManyToMany.HasLink(LeftRecordB,RightRecordY),IsEqualTo(false),'Test 5 failed') AssertThat(ManyToMany.HasLink(LeftRecordB,RightRecordZ),IsEqualTo(false),'Test 6 failed') AssertThat(ManyToMany.HasLink(LeftRecordC,RightRecordX),IsEqualTo(false),'Test 7 failed') AssertThat(ManyToMany.HasLink(LeftRecordC,RightRecordY),IsEqualTo(true), 'Test 8 failed') AssertThat(ManyToMany.HasLink(LeftRecordC,RightRecordZ),IsEqualTo(true), 'Test 9 failed')
Here's the most important bit: I haven't written the class code yet!
Step one, then, is to write the idealized, uncompilable unit test.Â
Step two is to get a clean compile and a failed test. If the test doesn't fail at this stage then it's clearly not a good test!
I do have an empty class on hand, so I need to include that globally:
When I compile I get a bunch of errors about the missing methods:
The next step is to create stub methods so everything compiles:
Here's the class header:
include('CML_IncludeInAllClassHeaderFiles.inc'),once CML_Data_ManyToManyLinks Class,Type,Module('CML_Data_ManyToManyLinks.CLW'),Link('CML_Data_ManyToManyLinks.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_) Construct Procedure() Destruct Procedure() AddLink procedure(long leftRecordID,long rightRecordID) HasLink procedure(long leftRecordID,long rightRecordID),bool End
And the methods:
CML_Data_ManyToManyLinks.Construct Procedure() code CML_Data_ManyToManyLinks.Destruct Procedure() code !dispose(self.Errors) CML_Data_ManyToManyLinks.AddLink procedure(long leftRecordID,long rightRecordID) code CML_Data_ManyToManyLinks.HasLink procedure(long leftRecordID,long rightRecordID)!,bool code return false
Now the unit test app compiles, but the test fails, as expected:
All I need to do now is write the minimum code necessary to get the code to pass.
It isn't possible to declare an actual queue inside a class, so I've created a typed queue and a reference in the class:
CML_Data_ManyToManyLinks_DataQ queue,TYPE LeftRecordID long RightRecordID long end CML_Data_ManyToManyLinks Class,Type,Module('CML_Data_ManyToManyLinks.CLW'),Link('CML_Data_ManyToManyLinks.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_) LinksQ &CML_Data_ManyToManyLinks_DataQ Construct Procedure() Destruct Procedure() AddLink procedure(long leftRecordID,long rightRecordID) HasLink procedure(long leftRecordID,long rightRecordID),bool End
The constructor creates an instance of the queue; the destructor empties and disposes of the queue. The AddLink and HasLink methods are almost identical except that one is looking up a record and the other one is adding a record.Â
CML_Data_ManyToManyLinks.Construct Procedure() code self.LinksQ &= new CML_Data_ManyToManyLinks_DataQ CML_Data_ManyToManyLinks.Destruct Procedure() code free(self.LinksQ) dispose(self.LinksQ) CML_Data_ManyToManyLinks.AddLink procedure(long leftRecordID,long rightRecordID) code clear(self.LinksQ) self.LinksQ.LeftRecordID = LeftRecordID self.LinksQ.RightRecordID = rightRecordID add(self.LinksQ,self.LinksQ.LeftRecordID,self.LinksQ.RightRecordID) CML_Data_ManyToManyLinks.HasLink procedure(long leftRecordID,long rightRecordID)!,bool code self.LinksQ.LeftRecordID = LeftRecordID self.LinksQ.RightRecordID = rightRecordID get(self.LinksQ,self.LinksQ.LeftRecordID,self.LinksQ.RightRecordID) if not errorcode() then return true. return false
Now my test succeeds:
So far so good. But can I wire this into my UITest program?
Using the tested class
My UITest program used this queue declaration:
CheckboxQueue queue Checked bool CheckedIcon long CheckedText string(30) end
But I'll need an ID field now, because I need to be able to identify my "right" record in the CML_Data_ManyToManyLinks class:
CheckboxQueue queue Checked bool CheckedIcon long CheckedText string(30) ID long end
Now I can declare an instance of my class:
Links CML_Data_ManyToManyLinks
In the code I'll initialize the queue of data and the linking class separately. If I were using this in a database setting, and using this code to display which classes a student takes, the first loop would be reading the list of potential classes and the second loop would be reading the records that indicate which classes the student actually takes (except that for purposes of demonstration I'm randomly selecting about half of them).Â
loop x = 1 to 100 clear(CheckboxQueue) !CheckboxQueue.FirstField = x * 10 CheckboxQueue.CheckedText = 'String ' & x CheckboxQueue.ID = x add(CheckboxQueue) end loop x = 1 to 100 if Random(1,2) = 1 Links.AddLink(x) end end Â
But there's something wrong with my code. I only have one parameter to AddLink, not two. This is effectively the "right" side value; there really isn't any particular reason to store the "left" side ID because there is only one record on the left side. I could create a default value for the first parameter, but that would mean omitting the first parameter and forgetting to do that is an easy mistake to make.Â
This doesn't mean there's anything fundamentally wrong with the class design; there might be a situation somewhere down the road where I really do want to keep links for multiple left records. But probably most uses of the class will only have one left record and multiple right records.Â
I added a LeftRecordID property to the class, which defaults to zero, and two new methods. I also renamed the existing methods:
CML_Data_ManyToManyLinks Class,Type,Module('CML_Data_ManyToManyLinks.CLW'),Link('CML_Data_ManyToManyLinks.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_) LinksQ &CML_Data_ManyToManyLinks_DataQ LeftRecordID long Construct Procedure() Destruct Procedure() IsLinkBetween procedure(long leftRecordID,long rightRecordID),bool IsLinkedTo procedure(long rightRecordID),bool SetLinkBetween procedure(long leftRecordID,long rightRecordID) SetLinkTo procedure(long rightRecordID) End
The naming isn't strictly consistent (IsLink vs IsLinked) but I think it's a bit better grammatically.Â
Here is the method code:
CML_Data_ManyToManyLinks.IsLinkedTo procedure(long rightRecordID)!,bool code return self.IsLinkBetween(self.LeftRecordID,rightRecordID) CML_Data_ManyToManyLinks.IsLinkBetween procedure(long leftRecordID,long rightRecordID)!,bool code self.LinksQ.LeftRecordID = LeftRecordID self.LinksQ.RightRecordID = rightRecordID get(self.LinksQ,self.LinksQ.LeftRecordID,self.LinksQ.RightRecordID) if not errorcode() then return true. return false CML_Data_ManyToManyLinks.SetLinkTo procedure(long rightRecordID) code self.SetLinkBetween(self.LeftRecordID,rightRecordID) CML_Data_ManyToManyLinks.SetLinkBetween procedure(long leftRecordID,long rightRecordID) code clear(self.LinksQ) self.LinksQ.LeftRecordID = LeftRecordID self.LinksQ.RightRecordID = rightRecordID add(self.LinksQ,self.LinksQ.LeftRecordID,self.LinksQ.RightRecordID)
I renamed the unit test procedure and added a second one:
Here's the code for the second test:
ManyToMany.SetLinkTo(RightRecordX) ManyToMany.SetLinkTo(RightRecordZ) AssertThat(ManyToMany.IsLinkedTo(RightRecordX),IsEqualTo(true), 'Test 1 failed') AssertThat(ManyToMany.IsLinkedTo(RightRecordY),IsEqualTo(false),'Test 2 failed') AssertThat(ManyToMany.IsLinkedTo(RightRecordZ),IsEqualTo(true), 'Test 3 failed')