-- Unit tests for [[Module:Example]]. Click talk page to run tests.localp=require('Module:UnitTests')functionp:test_hello()self:preprocess_equals('Hello World!','Hello World!')endreturnp
The talk page Module talk:Example/testcases executes it with {{#invoke: Example/testcases | run_tests}}. Test methods like test_hello above must begin with "test".
Methods
run_tests
run_tests: Runs all tests. Normally used on talk page of unit tests.
{{#invoke:Example/testcases|run_tests}}
If differs_at is specified, a column will be added showing the first character position where the expected and actual results differ.
If live_sandbox is specified, the header will show the columns "Test", "Live", "Sandbox", "Expected". This is required when using the preprocess_equals_sandbox_many method.
preprocess_equals
preprocess_equals(text, expected, options): Gives a piece of wikitext to preprocess and an expected resulting value. Scripts and templates can be invoked in the same manner they would be in a page.
preprocess_equals_many(prefix, suffix, cases, options): Performs a series of preprocess_equals() calls on a set of given pairs. Automatically adds the given prefix and suffix to each text.
preprocess_equals_preprocess(text, expected, options): Gives two pieces of wikitext to preprocess and determines if they produce the same value. Useful for comparing scripts to existing templates.
preprocess_equals_preprocess_many(prefix1, suffix1, prefix2, suffix2, cases, options): Performs a series of preprocess_equals_preprocess() calls on a set of given pairs. The prefix/suffix supplied for both arguments is added automatically. If in any case the second part is not specified, the first part will be used.
self:preprocess_equals_preprocess_many('{{#invoke:ConvertNumeric | numeral_to_english|','}}','{{spellnum','}}',{{'2'},-- equivalent to {'2','2'},{'-2','-2.0'},},{nowiki=1})
preprocess_equals_sandbox_many
preprocess_equals_sandbox_many(module, function, cases, options): Performs a series of preprocess_equals_compare() calls on a set of given pairs. The test compares the live version of the module vs the /sandbox version and vs an expected result. Ensure live_sandbox is specified or there may be some errors in the output.
equals(name, actual, expected, options): Gives a computed value and the expected value, and checks if they are equal according to the == operator. Useful for testing modules that are designed to be used by other modules rather than using #invoke.
self:equals('Simple addition',2+2,4,{nowiki=1})
equals_deep
equals_deep(name, actual, expected, options): Like equals, but handles tables by doing a deep comparison. Neither value should contain circular references, as they are not handled by the current implementation and may result in an infinite loop.
-- UnitTester provides unit testing for other Lua scripts. For details see [[Wikipedia:Lua#Unit_testing]].-- For user documentation see talk page.localUnitTester={}localframe,tick,cross,should_highlightlocalresult_table_header="{|class=\"wikitable unit-tests-result\"\n|+ %s\n! !! Text !! Expected !! Actual"localresult_table_live_sandbox_header="{|class=\"wikitable unit-tests-result\"\n|+ %s\n! !! Test !! Live !! Sandbox !! Expected"localresult_table={n=0}localresult_table_mt={insert=function(self,...)localn=self.nfori=1,select('#',...)dolocalval=select(i,...)ifval~=nilthenn=n+1self[n]=valendendself.n=nend,insert_format=function(self,...)self:insert(string.format(...))end,concat=table.concat}result_table_mt.__index=result_table_mtsetmetatable(result_table,result_table_mt)localnum_failures=0localnum_runs=0localfunctionfirst_difference(s1,s2)s1,s2=tostring(s1),tostring(s2)ifs1==s2thenreturn''endlocalmax=math.min(#s1,#s2)fori=1,maxdoifs1:sub(i,i)~=s2:sub(i,i)thenreturniendendreturnmax+1endlocalfunctionreturn_varargs(...)return...endfunctionUnitTester:calculate_output(text,expected,actual,options)-- Set up some variables for throughout for easenum_runs=num_runs+1localoptions=optionsor{}-- Fix any stripmarkers if asked to do so to prevent incorrect failslocalcompared_expected=expectedlocalcompared_actual=actualifoptions.templatestylesthenlocalpattern='(\127[^\127]*UNIQ%-%-templatestyles%-)(%x+)(%-QINU[^\127]*\127)'local_,expected_stripmarker_id=compared_expected:match(pattern)-- when module rendering has templatestyles strip markers, use ID from expected to prevent false test failifexpected_stripmarker_idthencompared_actual=compared_actual:gsub(pattern,'%1'..expected_stripmarker_id..'%3')-- replace actual id with expected id; ignore second capture in patterncompared_expected=compared_expected:gsub(pattern,'%1'..expected_stripmarker_id..'%3')-- account for other strip markersendendifoptions.stripmarkerthenlocalpattern='(\127[^\127]*UNIQ%-%-%l+%-)(%x+)(%-%-?QINU[^\127]*\127)'local_,expected_stripmarker_id=compared_expected:match(pattern)ifexpected_stripmarker_idthencompared_actual=compared_actual:gsub(pattern,'%1'..expected_stripmarker_id..'%3')compared_expected=compared_expected:gsub(pattern,'%1'..expected_stripmarker_id..'%3')endend-- Perform the comparisonlocalsuccess=compared_actual==compared_expectedifnotsuccessthennum_failures=num_failures+1end-- Sort the wikitext for displaying the resultsifoptions.combinedthen-- We need 2 rows available for the expected and actual columns-- Top one is parsed, bottom is unparsedlocaldiffers_at=self.differs_atand(' \n| rowspan=2|'..first_difference(compared_expected,compared_actual))or''-- Local copies of tick/cross to allow for highlightinglocalhighlight=(should_highlightandnotsuccessand'style="background:#fc0;" ')or''result_table:insert(-- Start output'| ',highlight,'rowspan=2|',successandtickorcross,-- Tick/Cross (2 rows)' \n| rowspan=2|',mw.text.nowiki(text),' \n| ',-- Text used for the test (2 rows)expected,' \n| ',actual,-- The parsed outputs (in the 1st row)differs_at,' \n|-\n| ',-- Where any relevant difference was (2 rows)mw.text.nowiki(expected),' \n| ',mw.text.nowiki(actual),-- The unparsed outputs (in the 2nd row)'\n|-\n'-- End output)else-- Display normally with whichever option was preferred (nowiki/parsed)localdiffers_at=self.differs_atand(' \n| '..first_difference(compared_expected,compared_actual))or''localformatting=options.nowikiandmw.text.nowikiorreturn_varargslocalhighlight=(should_highlightandnotsuccessand'style="background:#fc0;"|')or''result_table:insert(-- Start output'| ',highlight,successandtickorcross,-- Tick/Cross' \n| ',mw.text.nowiki(text),' \n| ',-- Text used for the testformatting(expected),' \n| ',formatting(actual),-- The formatted outputsdiffers_at,-- Where any relevant difference was'\n|-\n'-- End output)endendfunctionUnitTester:preprocess_equals(text,expected,options)localactual=frame:preprocess(text)self:calculate_output(text,expected,actual,options)endfunctionUnitTester:preprocess_equals_many(prefix,suffix,cases,options)for_,caseinipairs(cases)doself:preprocess_equals(prefix..case[1]..suffix,case[2],options)endendfunctionUnitTester:preprocess_equals_preprocess(text1,text2,options)localactual=frame:preprocess(text1)localexpected=frame:preprocess(text2)self:calculate_output(text1,expected,actual,options)endfunctionUnitTester:preprocess_equals_compare(live,sandbox,expected,options)locallive_text=frame:preprocess(live)localsandbox_text=frame:preprocess(sandbox)localhighlight_live=falselocalhighlight_sandbox=falsenum_runs=num_runs+1iflive_text==expectedandsandbox_text==expectedthenresult_table:insert('| ',tick)elseresult_table:insert('| ',cross)num_failures=num_failures+1iflive_text~=expectedthenhighlight_live=trueendifsandbox_text~=expectedthenhighlight_sandbox=trueendendlocalformatting=(optionsandoptions.nowikiandmw.text.nowiki)orreturn_varargslocaldiffers_at=self.differs_atand(' \n| '..first_difference(expected,live_text)orfirst_difference(expected,sandbox_text))or''result_table:insert(' \n| ',mw.text.nowiki(live),should_highlightandhighlight_liveand' \n|style="background: #fc0;"| 'or' \n| ',formatting(live_text),should_highlightandhighlight_sandboxand' \n|style="background: #fc0;"| 'or' \n| ',formatting(sandbox_text),' \n| ',formatting(expected),differs_at,"\n|-\n")endfunctionUnitTester:preprocess_equals_preprocess_many(prefix1,suffix1,prefix2,suffix2,cases,options)for_,caseinipairs(cases)doself:preprocess_equals_preprocess(prefix1..case[1]..suffix1,prefix2..(case[2]andcase[2]orcase[1])..suffix2,options)endendfunctionUnitTester:preprocess_equals_sandbox_many(module,function_name,cases,options)for_,caseinipairs(cases)dolocallive=module.."|"..function_name.."|"..case[1].."}}"localsandbox=module.."/sandbox|"..function_name.."|"..case[1].."}}"self:preprocess_equals_compare(live,sandbox,case[2],options)endendfunctionUnitTester:equals(name,actual,expected,options)num_runs=num_runs+1ifactual==expectedthenresult_table:insert('| ',tick)elseresult_table:insert('| ',cross)num_failures=num_failures+1endlocalformatting=(optionsandoptions.nowikiandmw.text.nowiki)orreturn_varargslocaldiffers_at=self.differs_atand(' \n| '..first_difference(expected,actual))or''localdisplay=optionsandoptions.displayorreturn_varargsresult_table:insert(' \n| ',name,' \n| ',formatting(tostring(display(expected))),' \n| ',formatting(tostring(display(actual))),differs_at,"\n|-\n")endlocalfunctiondeep_compare(t1,t2,ignore_mt)localty1=type(t1)localty2=type(t2)ifty1~=ty2thenreturnfalseendifty1~='table'andty2~='table'thenreturnt1==t2endlocalmt=getmetatable(t1)ifnotignore_mtandmtandmt.__eqthenreturnt1==t2endfork1,v1inpairs(t1)dolocalv2=t2[k1]ifv2==nilornotdeep_compare(v1,v2)thenreturnfalseendendfork2,v2inpairs(t2)dolocalv1=t1[k2]ifv1==nilornotdeep_compare(v1,v2)thenreturnfalseendendreturntrueendlocalfunctionval_to_str(obj)localfunctiontable_key_to_str(k)iftype(k)=='string'andmw.ustring.match(k,'^[_%a][_%a%d]*$')thenreturnkelsereturn'['..val_to_str(k)..']'endendiftype(obj)=="string"thenobj=mw.ustring.gsub(obj,"\n","\\n")ifmw.ustring.match(mw.ustring.gsub(obj,'[^\'"]',''),'^"+$')thenreturn"'"..obj.."'"endreturn'"'..mw.ustring.gsub(obj,'"','\\"')..'"'elseiftype(obj)=="table"thenlocalresult,checked={},{}fork,vinipairs(obj)dotable.insert(result,val_to_str(v))checked[k]=trueendfork,vinpairs(obj)doifnotchecked[k]thentable.insert(result,table_key_to_str(k)..'='..val_to_str(v))endendreturn'{'..table.concat(result,',')..'}'elsereturntostring(obj)endendfunctionUnitTester:equals_deep(name,actual,expected,options)num_runs=num_runs+1ifdeep_compare(actual,expected)thenresult_table:insert('| ',tick)elseresult_table:insert('| ',cross)num_failures=num_failures+1endlocalformatting=(optionsandoptions.nowikiandmw.text.nowiki)orreturn_varargslocalactual_str=val_to_str(actual)localexpected_str=val_to_str(expected)localdiffers_at=self.differs_atand(' \n| '..first_difference(expected_str,actual_str))or''result_table:insert(' \n| ',name,' \n| ',formatting(expected_str),' \n| ',formatting(actual_str),differs_at,"\n|-\n")endfunctionUnitTester:iterate(examples,func)require'libraryUtil'.checkType('iterate',1,examples,'table')iftype(func)=='string'thenfunc=self[func]elseiftype(func)~='function'thenerror(("bad argument #2 to 'iterate' (expected function or string, got %s)"):format(type(func)),2)endfori,exampleinipairs(examples)doiftype(example)=='table'thenfunc(self,unpack(example))elseiftype(example)=='string'thenself:heading(example)elseerror(('bad example #%d (expected table, got %s)'):format(i,type(example)),2)endendendfunctionUnitTester:heading(text)result_table:insert_format(' ! colspan="%u" style="text-align: left" | %s \n |- \n ',self.columns,text)endfunctionUnitTester:run(frame_arg)frame=frame_argself.frame=frameself.differs_at=frame.args['differs_at']tick=frame:preprocess('{{Tick}}')cross=frame:preprocess('{{Cross}}')localtable_header=result_table_headerifframe.args['live_sandbox']thentable_header=result_table_live_sandbox_headerendifframe.args.highlightthenshould_highlight=trueendself.columns=4ifself.differs_atthentable_header=table_header..' !! Differs at'self.columns=self.columns+1end-- Sort results into alphabetical order.localself_sorted={}forkey,_inpairs(self)doifkey:find('^test')thentable.insert(self_sorted,key)endendtable.sort(self_sorted)-- Add results to the results table.for_,valueinipairs(self_sorted)doresult_table:insert_format(table_header.."\n|-\n",value)self[value](self)result_table:insert("|}\n")endreturn(num_runs==0and"<b>No tests were run.</b>"ornum_failures==0and"<b style=\"color:#008000\">All "..num_runs.." tests passed.</b>"or"<b style=\"color:#800000\">"..num_failures.." of "..num_runs.." tests failed.</b>[[Category:Failed Lua testcases using Module:UnitTests]]").."\n\n"..frame:preprocess(result_table:concat())endfunctionUnitTester:new()localo={}setmetatable(o,self)self.__index=selfreturnoendlocalp=UnitTester:new()functionp.run_tests(frame)returnp:run(frame)endreturnp