Files 1
Downloads 1,799
Favorites 2
My AddOns
    Safety
    Practical Safety: How to test a Print function
    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(expectedsampleMsgRecord);
        
        
    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(expectedactualfalsefalse);
    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(scrollingMessageFrameKU_TESTS_ERR_SetUpMockScrollingMessageFrame_NIL_FRAME);
        
    local frameId scrollingMessageFrame:GetName();
        
    KUtils.Safety.RejectNil(frameIdKU_TESTS_ERR_SetUpMockScrollingMessageFrame_NIL_FRAME_ID);
        
        
    local list = KUtils.Safety.MockScrollingMessageFrame.ListStdout;
        if(list[
    frameId]==nilthen
            
    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 framewe 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(selfmsgrgbmsgId)
        
    local msgRecord KUtils.Safety.MockScrollingMessageFrameRecord:new(msgrgbmsgId);
        
    local frameId self:GetName();
        
    KUtils.Safety.RejectNil(frameIdKU_TESTS_ERR_SpyAddMessage_NIL_SELF_ID);
        
        
    local list = KUtils.Safety.MockScrollingMessageFrame.ListRecords;
        if(list[
    frameId]==nilthen
            
    list[frameId]={};
        
    end
        local frameList 
    = list[frameId];
        
    table.insert(frameListmsgRecord);
        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(msgrgbid)
        
    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(frameIdKU_TESTS_ERR_ClearMockScrollingMessageFrameRecords_NIL_FRAME_ID);

        
    local list = KUtils.Safety.MockScrollingMessageFrame.ListRecords;
        if(list[
    frameId]~=nilthen
            
    list[frameId]=nil;
            
    KUtils.Safety.MockScrollingMessageFrame.ListRecords=list;
        
    end
    end

    --------------------------------------------------------------------------------
    function 
    KUtils.Safety:MockScrollingMessageFrameRecords(frameId)
        
    KUtils.Safety.RejectNil(frameIdKU_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(scrollingMessageFrameKU_TESTS_ERR_TearDownMockScrollingMessageFrame_NIL_FRAME);
        
    local frameId scrollingMessageFrame:GetName();
        
    KUtils.Safety.RejectNil(frameIdKU_TESTS_ERR_TearDownMockScrollingMessageFrame_NIL_FRAME_ID);
        
        
    local list = KUtils.Safety.MockScrollingMessageFrame.ListStdout;
        if(list[
    frameId]~=nilthen
            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 framewe 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(expectedListactualListcheckColorscheckIds)
        
    local expectedSize table.getn(expectedList);
        
    local actualSize table.getn(actualList);
        
    Safety.Assert:AreEqual(expectedSizeactualSize"Mismatching List sizes.");
        
        
    local expectedIndexexpectedRecord next(expectedListnil);
        
    local actualIndexactualRecord next(actualListnil);
        while 
    expectedIndex do
            
    Safety.Assert:AreEqual(expectedIndexactualIndex"Mismatching Indices: expected='"..expectedIndex..
                
    "', actual='"..actualIndex.."'.");
            
            
    local expectedType type(expectedRecord);
            
    local actualType type(actualRecord);
            
    Safety.Assert:AreEqual(expectedTypeactualType"Mismatching Value Types: expected='"..expectedType..
                
    "', actual='"..actualType.."', at index ='"..expectedIndex.."'.");
            
            --
    Assume the values are KUtils.Safety.MockScrollingMessageFrameRecord objects
            KUtils
    .Safety.AssertMatchingScrollingMessageFrameRecords(expectedRecordactualRecordcheckColorcheckId,
                
    "Mismatching Scrolling Message Frame Records at index '"..expectedIndex.."': ");
            
            
    expectedIndexexpectedRecord next(expectedListexpectedIndex);
            
    actualIndexactualRecord next(actualListactualIndex);
        
    end
    end

    --------------------------------------------------------------------------------
    function 
    KUtils.Safety.AssertMatchingScrollingMessageFrameRecords(expectedactualcheckColorcheckIderrMsg)
        
    Safety.Assert:AreEqual(expected.Messageactual.MessageerrMsg.."Mismatching Message attribute: "..
            
    "expected='"..KUtils:DeNillify(expected.Message).."', actual='"..KUtils:DeNillify(actual.Message).."'.");
        if(
    checkColorthen
            Safety
    .Assert:AreEqual(expected.Redactual.RederrMsg.."Mismatching Red attribute: "..
                
    "expected='"..KUtils:DeNillify(expected.Red).."', actual='"..KUtils:DeNillify(actual.Red).."'.");
            
    Safety.Assert:AreEqual(expected.Greenactual.GreenerrMsg.."Mismatching Green attribute: "..
                
    "expected='"..KUtils:DeNillify(expected.Green).."', actual='"..KUtils:DeNillify(actual.Green).."'.");
            
    Safety.Assert:AreEqual(expected.Blueactual.BlueerrMsg.."Mismatching Blue attribute: "..
                
    "expected='"..KUtils:DeNillify(expected.Blue).."', actual='"..KUtils:DeNillify(actual.Blue).."'.");
        
    end
        
    if(checkIdthen
            Safety
    .Assert:AreEqual(expected.Idactual.IderrMsg.."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.