How to test a Print function
I think the Print function is one of the most common functions among addons. You know the function I'm talking about. I wrote one in my KUtils library:
PHP Code:
function KUtils:Print(msg)
msg = KUtils:DeNillify(msg);
DEFAULT_CHAT_FRAME:AddMessage(msg);
end
(don't worry about the KUtils:DeNillify call for now.. it simple checks if the argument we pass it is nil, and, if so, returns me a '[nil]' string so that I can print it).
Of course, one has to ask: how do I test this function ?
The Plan
In short, what we want to do is:
* Hook into the DEFAULT_CHAT_FRAME AddMessage function with our own function
* Have our own function remember the messages that it is asked to print
* Exercise our Print function with an appropriate Safety test
In the code I'll share below things are a bit more complicated because, when I wrote it, I wanted to make sure I could support more complex scenarios (such as intercepting the AddMessage calls for multiple frames at the same time).
Safety First
Of course, we start with a Safety test. Since I'm writing the test for the KUtils library, I place this code in a KUtils_Tests.lua file in the KUtils addon:
PHP Code:
KUtils.Safety = {};
--------------------------------------------------------------------------------
function KUtils.Safety:RegisterSuite()
local suite = TestSuite:new("KUtils Test Suite", "Tests for KUtils");
suite:Add("Test_Print", KUtils.Safety.Test_Print);
Safety.SuiteRegistry:Register(suite);
end
--------------------------------------------------------------------------------
function KUtils.Safety:Test_Print()
KUtils.Safety:SetUpMockScrollingMessageFrame(DEFAULT_CHAT_FRAME);
KUtils.Safety:ClearMockScrollingMessageFrameRecords(DEFAULT_CHAT_FRAME:GetName());
local sampleMsg = "Hello World !";
local expected = {};
KUtils:Print(sampleMsg);
local sampleMsgRecord = KUtils.Safety.MockScrollingMessageFrameRecord:new(sampleMsg);
table.insert(expected, sampleMsgRecord);
local actual = KUtils.Safety:MockScrollingMessageFrameRecords(DEFAULT_CHAT_FRAME:GetName());
KUtils.Safety:TearDownMockScrollingMessageFrame(DEFAULT_CHAT_FRAME);
KUtils.Safety:ClearMockScrollingMessageFrameRecords(DEFAULT_CHAT_FRAME:GetName());
KUtils.Safety:AssertMatchingScrollingMessageFrameRecordsLists(expected, actual, false, false);
end
In other words, we create a KUtils.Safety namespace, add a test method (KUtils.Safety:Test_Print), and a KUtils.Safety:RegisterSuite method that creates a test suite and registers it with the Safety Suite Registry so that we can use Safety's GUI component to run the test. The RegisterSuite method is called by the initialization method for the KUtils addon.
The test method itself does the following:
* Set up the DEFAULT_CHAT_FRAME to use our own function when it receives a request to AddMessage;
* Clear the list of messages saved by our own function for this frame;
* Exercise the KUtils:Print function with a known message;
* Set up an expected list of message records based ont he message we fed to KUtils:Print;
* Retrieve the list of messages our own AddMessage function recorded when we exercised KUtils:Print;
* Clean-up (i.e. reset DEFAULT_CHAT_FRAME to use its own AddMessage, and clear the records that our own AddMessage saves);
* Verify that the expected and actual lists of messages match;
As you can see int he code above, most of this is done by calling other functions.
Set Up a Scrolling Message Frame to use our own function instead of AddMessage
Here's the function that we call to modify the behavior of a given ScrollingMessageFrame (DEFAULT_CHAT_FRAME is one of these):
PHP Code:
--------------------------------------------------------------------------------
function KUtils.Safety:SetUpMockScrollingMessageFrame(scrollingMessageFrame)
KUtils.Safety.RejectNil(scrollingMessageFrame, KU_TESTS_ERR_SetUpMockScrollingMessageFrame_NIL_FRAME);
local frameId = scrollingMessageFrame:GetName();
KUtils.Safety.RejectNil(frameId, KU_TESTS_ERR_SetUpMockScrollingMessageFrame_NIL_FRAME_ID);
local list = KUtils.Safety.MockScrollingMessageFrame.ListStdout;
if(list[frameId]==nil) then
list[frameId]=scrollingMessageFrame.AddMessage;
scrollingMessageFrame.AddMessage = KUtils.Safety.MockScrollingMessageFrame.SpyAddMessage;
KUtils.Safety.MockScrollingMessageFrame.ListStdout=list;
end
--else: the list already contained a record for this frame: we do nothing
end
Don't worry about the calls to KUtils.Safety:RejectNil you'll see in the code. That's a simple function that checks if the given argument is nil. If so, it generates an error based on the given second argument.
The important stuff, in this function, is the if statement. We first check if there is a record in our KUtils.Safety.MockScrollingMessageFrame.ListStdout list indexed under the Id of the frame we were passed.
This KUtils.Safety.MockScrollingMessageFrame.ListStdout list is a list we use to keep the original AddMessage functions when we hook our own function in their place, so that later (during the Clean-up step) we can reset everything as it should.
If the ListStdout contains a record for our frame id, then we don't do anything else, since the frame has already been set up to use our own method. Otherwise, we save the frame's AddMessage function in the ListStdout list and tell the frame to use our very own KUtils.Safety.MockScrollingMessageFrame.SpyAddMessage function when someone asks it to AddMessage.
Spying Messages and Message Records
The basic idea behind our own AddMessage function (i.e. the KUtils.Safety.MockScrollingMessageFrame.SpyAddMessage function) is that of capturing the messages it receives in some list that we can later access to compare with an expected list. Here's the code for our function:
PHP Code:
--------------------------------------------------------------------------------
function KUtils.Safety.MockScrollingMessageFrame.SpyAddMessage(self, msg, r, g, b, msgId)
local msgRecord = KUtils.Safety.MockScrollingMessageFrameRecord:new(msg, r, g, b, msgId);
local frameId = self:GetName();
KUtils.Safety.RejectNil(frameId, KU_TESTS_ERR_SpyAddMessage_NIL_SELF_ID);
local list = KUtils.Safety.MockScrollingMessageFrame.ListRecords;
if(list[frameId]==nil) then
list[frameId]={};
end
local frameList = list[frameId];
table.insert(frameList, msgRecord);
list[frameId]=frameList;
KUtils.Safety.MockScrollingMessageFrame.ListRecords=list;
end
The signature of the function includes a self argument which is actually a reference to the frame whose AddMessage call we are intercepting. As you can see, we create a new KUtils.Safety.MockScrollingMessageFrameRecord object and then store it in the KUtils.Safety.MockScrollingMessageFrame.ListRecords list, in an ordered sublist under an index matching the frame id.
Nothign fancy. Well, maybe except for creating a record for the message. If you've never seen any object-oriented lua, this will seem very fancy. I'm not going to explain how it works, but here's the constructor for our record:
PHP Code:
KUtils.Safety.MockScrollingMessageFrameRecord = {};
--------------------------------------------------------------------------------
function KUtils.Safety.MockScrollingMessageFrameRecord:new(msg, r, g, b, id)
local object =
{
Message = msg,
Red = r,
Green = g,
Blue = b,
Id = id,
};
setmetatable(object, {__index = KUtils.Safety.MockScrollingMessageFrameRecord});
return object;
end
Managing Message Records
In our test, we call a couple of functions dealing with the message records generated by our SpyAddMessage function:
PHP Code:
--------------------------------------------------------------------------------
function KUtils.Safety:ClearMockScrollingMessageFrameRecords(frameId)
KUtils.Safety.RejectNil(frameId, KU_TESTS_ERR_ClearMockScrollingMessageFrameRecords_NIL_FRAME_ID);
local list = KUtils.Safety.MockScrollingMessageFrame.ListRecords;
if(list[frameId]~=nil) then
list[frameId]=nil;
KUtils.Safety.MockScrollingMessageFrame.ListRecords=list;
end
end
--------------------------------------------------------------------------------
function KUtils.Safety:MockScrollingMessageFrameRecords(frameId)
KUtils.Safety.RejectNil(frameId, KU_TESTS_ERR_MockScrollingMessageFrameRecords_NIL_FRAME_ID);
local list = KUtils.Safety.MockScrollingMessageFrame.ListRecords;
return list[frameId];
end
The first of these functions is used at the beginning and end of our test, to make sure that the list of messages we intercept for our DEFAULT_CHAT_FRAME is empty when we start, and empty after we're done.
The second function is simply a quick way to retrieve the list of message records we intercepted for a given frame.
Tear Down; Resetting the frame's original AddMessage
At the end of our test, we certainly want to leave the frame we are working with (DEFAULT_CHAT_FRAME) as we found it in the beginning. Here's the function we call to reset its AddMessage handler:
PHP Code:
--------------------------------------------------------------------------------
function KUtils.Safety:TearDownMockScrollingMessageFrame(scrollingMessageFrame)
KUtils.Safety.RejectNil(scrollingMessageFrame, KU_TESTS_ERR_TearDownMockScrollingMessageFrame_NIL_FRAME);
local frameId = scrollingMessageFrame:GetName();
KUtils.Safety.RejectNil(frameId, KU_TESTS_ERR_TearDownMockScrollingMessageFrame_NIL_FRAME_ID);
local list = KUtils.Safety.MockScrollingMessageFrame.ListStdout;
if(list[frameId]~=nil) then
local stdout = list[frameId];
scrollingMessageFrame.AddMessage=stdout;
list[frameId]=nil;
KUtils.Safety.MockScrollingMessageFrame.ListStdout=list;
end
--else: the list did not contain a record for this frame: we do nothing
end
Simply put, we expect to find the original function in the KUtils.Safety.MockScrollingMessageFrame.ListStdout list.
Comparing Lists of Message Records
The last part of our test is about comparing two lists of message records: the expected one, and the actual one we recorded during the test. Here are two functions used to do this. The first checks that the two lists have the same size (it assumes the lists are indexed using integral values), and then iterates over the lists; the second function deals with comparing two specific records.
PHP Code:
--------------------------------------------------------------------------------
function KUtils.Safety:AssertMatchingScrollingMessageFrameRecordsLists(expectedList, actualList, checkColors, checkIds)
local expectedSize = table.getn(expectedList);
local actualSize = table.getn(actualList);
Safety.Assert:AreEqual(expectedSize, actualSize, "Mismatching List sizes.");
local expectedIndex, expectedRecord = next(expectedList, nil);
local actualIndex, actualRecord = next(actualList, nil);
while expectedIndex do
Safety.Assert:AreEqual(expectedIndex, actualIndex, "Mismatching Indices: expected='"..expectedIndex..
"', actual='"..actualIndex.."'.");
local expectedType = type(expectedRecord);
local actualType = type(actualRecord);
Safety.Assert:AreEqual(expectedType, actualType, "Mismatching Value Types: expected='"..expectedType..
"', actual='"..actualType.."', at index ='"..expectedIndex.."'.");
--Assume the values are KUtils.Safety.MockScrollingMessageFrameRecord objects
KUtils.Safety.AssertMatchingScrollingMessageFrameRecords(expectedRecord, actualRecord, checkColor, checkId,
"Mismatching Scrolling Message Frame Records at index '"..expectedIndex.."': ");
expectedIndex, expectedRecord = next(expectedList, expectedIndex);
actualIndex, actualRecord = next(actualList, actualIndex);
end
end
--------------------------------------------------------------------------------
function KUtils.Safety.AssertMatchingScrollingMessageFrameRecords(expected, actual, checkColor, checkId, errMsg)
Safety.Assert:AreEqual(expected.Message, actual.Message, errMsg.."Mismatching Message attribute: "..
"expected='"..KUtils:DeNillify(expected.Message).."', actual='"..KUtils:DeNillify(actual.Message).."'.");
if(checkColor) then
Safety.Assert:AreEqual(expected.Red, actual.Red, errMsg.."Mismatching Red attribute: "..
"expected='"..KUtils:DeNillify(expected.Red).."', actual='"..KUtils:DeNillify(actual.Red).."'.");
Safety.Assert:AreEqual(expected.Green, actual.Green, errMsg.."Mismatching Green attribute: "..
"expected='"..KUtils:DeNillify(expected.Green).."', actual='"..KUtils:DeNillify(actual.Green).."'.");
Safety.Assert:AreEqual(expected.Blue, actual.Blue, errMsg.."Mismatching Blue attribute: "..
"expected='"..KUtils:DeNillify(expected.Blue).."', actual='"..KUtils:DeNillify(actual.Blue).."'.");
end
if(checkId) then
Safety.Assert:AreEqual(expected.Id, actual.Id, errMsg.."Mismatching Id attribute: "..
"expected='"..KUtils:DeNillify(expected.Id).."', actual='"..KUtils:DeNillify(actual.Id).."'.");
end
end
And that's all, folks.
F.O.R.
PS: I wouldn't be surprised if the next version of Safety will include something similar to what I described here so that when you want to intercept the calls to a frame's AddMessage API, you'll be able to use a Safety component rather than write all of this yourself.