모듈:UnitTests
UnitTests 모듈은 다른 스크립트에서 사용할 수 있는 유닛 테스트 도구를 require를 통해 제공하고 있습니다. 자세한 설명은 위키백과:루아#유닛 테스트를 참고하세요.
아래는 예제 코드로써 UnitTests를 사용하는 모습을 볼 수 있는 모듈:Bananas/시험장의 코드입니다.
-- [[모듈:Bananas]]의 테스트 케이스입니다. 테스트를 하려면 '토론'을 클릭하세요.
local p = require('Module:UnitTests')
function p:test_hello()
self:preprocess_equals('{{#invoke:Bananas | hello}}', 'Hello, world!')
end
return p
토론 문서인 모듈토론:Bananas/시험장에선 모듈:Bananas의 유닛 테스트를 실행한 결과가 출력되며, 유닛 테스트 결과를 출력하기 위해 {{#invoke: Bananas/testcases | run_tests}}를 사용하고 있는 것을 볼 수 있습니다.
참고로 테스트를 수행하는 테스트 메소드의 이름은 위 코드의 test_hello처럼 반드시 test로 시작되어야 합니다.
메소드
[편집]run_tests를 제외한 모든 메소드에서 매개변수 options를 선택적으로 사용할 수 있는 것을 볼 수 있는데, 이 매개변수의 값으로 { nowiki=1 }을 주면 테스트 결과가 출력될 때 실젯값과 기댓값이 위키텍스트로 처리되어 해석되지 않고 <nowiki>...</nowiki>를 사용한 것처럼 위키텍스트 그대로 출력됩니다.
run_tests
[편집]run_tests()run_tests(differs_at)
모든 테스트를 실행시킵니다. 매개변수 differs_at에 1이 주어진 경우, 기댓값과 실젯값이 다르기 시작한 첫번째 위치를 나타내는 열이 추가됩니다. 일반적으로 유닛 테스트 코드가 있는 모듈의 토론 문서(모듈토론)에서 사용됩니다.
{{ #invoke: Bananas/시험장 | run_tests }}
{{ #invoke: Bananas/시험장 | run_tests | differs_at=1 }}
preprocess_equals
[편집]preprocess_equals(text, expected)preprocess_equals(text, expected, options)
전처리 과정을 거쳐 실젯값을 출력해줄 위키텍스트와 평문으로 작성된 기댓값을 각각 매개변수 text와 expected로 받은 뒤, 전처리를 실행한 값과 기댓값이 같은지 비교하여 테스트의 성공 여부를 판별합니다. 스크립트와 틀은 문서에서 불러오는 방식과 동일한 방식으로 불러옵니다.
self:preprocess_equals('{{#invoke: Bananas | hello}}', 'Hello, world!', { nowiki=1 })
preprocess_equals_many
[편집]preprocess_equals_many(prefix, suffix, cases)preprocess_equals_many(prefix, suffix, cases, options)
테스트 케이스 묶음으로써 매개변수 cases로 받은 (실젯값을 출력해줄 위키텍스트, 평문 기댓값) 짝들을 메소드 preprocess_equals에 대입하며 반복 실행시킵니다. 이 때 매개변수 prefix와 suffix가 위키텍스트 앞뒤에 각각 접두/접미 문자열로써 자동으로 추가됩니다.
self:preprocess_equals_many('{{#invoke: BananasArgs | add |', '}}', {
{'2|3', '5'},
{'-2|2', '0'},
}, { nowiki=1 })
preprocess_equals_preprocess
[편집]preprocess_equals_preprocess(text, expected)preprocess_equals_preprocess(text, expected, options)
전처리 과정을 거쳐 실젯값을 출력해줄 위키텍스트 및 같은 방식으로 기댓값을 출력해줄 위키텍스트를 각각 매개변수 text와 expected로 받은 뒤, 두 위키텍스트의 처리 결과가 같은지 비교하여 테스트의 성공 여부를 판별합니다. 스크립트의 출력값과 위키백과에 먼저 있었던 틀의 출력값이 서로 일치하는지 확인하고자 할 때 유용합니다.
self:preprocess_equals_preprocess('{{#invoke: Bananas | hello}}', '{{Hello}}', { nowiki=1 })
preprocess_equals_preprocess_many
[편집]preprocess_equals_preprocess_many(prefix1, suffix1, prefix2, suffix2, cases)preprocess_equals_preprocess_many(prefix1, suffix1, prefix2, suffix2, cases, options)
테스트 케이스 묶음으로써 매개변수 cases로 받은 (실젯값을 출력해줄 위키텍스트, 기댓값을 출력해줄 위키텍스트) 짝들을 메소드 preprocess_equals_preprocess에 대입하며 반복 실행시킵니다. 이 때 매개변수 prefix1와 suffix1은 첫번째 부분(실젯값을 출력해줄 위키텍스트)의 앞뒤에, 매개변수 prefix2와 suffix2은 두번째 부분(기댓값을 출력해줄 위키텍스트)의 앞뒤에 각각 접두/접미 문자열로써 자동으로 추가됩니다. 테스트 케이스에서 두번째 부분(기댓값을 출력해줄 위키텍스트)이 따로 기재되지 않은 경우엔 기대값을 출력해줄 위키텍스트로써 첫번째 부분(실젯값을 출력해줄 위키텍스트)을 그대로 사용합니다.
self:preprocess_equals_preprocess_many('{{#invoke:Foo | spellnum |', '}}', '{{spellnum', '}}', {
{'2'}, -- {'2','2'}, 와 같음
{'-2', '-2.0'},
}, { nowiki=1 })
equals
[편집]equals(name, actual, expected)equals(name, actual, expected, options)
계산된 값과 기댓값을 각각 매개변수 actual와 expected로 받아서 == 연산자를 이용해 둘이 일치하는지 확인합니다. 다른 모듈에 쓰이도록 설계된 모듈을 테스트할 때 #invoke를 사용하는 것보다 유용합니다.
self:equals('Simple addition', 2 + 2, 4, { nowiki=1 })
equals_deep
[편집]equals_deep(name, actual, expected)equals_deep(name, actual, expected, options)
equals와 비슷하지만 매개변수 actual와 expected로 주어진 루아 테이블을 심층적인 비교를 하는 방식을 이용함으로써 서로 같은지 비교합니다. 이 때 어떤 값도 순환 참조를 포함해선 안되는데, 이는 현재의 구현상태에선 다루고 있지 않아 무한 루프에 빠질 수 있기 때문입니다.
self:equals_deep('Table comparison', createRange(1,3), {1,2,3}, { nowiki=1 })
같이 보기
[편집]- 모듈:ScribuntoUnit - UnitTests 대신 사용할 수 있는 유닛 테스트 모듈
-- UnitTester provides unit testing for other Lua scripts. For details see [[Wikipedia:Lua#Unit_testing]].
-- For user documentation see talk page.
local UnitTester = {}
local frame, tick, cross, should_highlight
local result_table_header = "{|class=\"wikitable unit-tests-result\"\n|+ %s\n! !! 텍스트 !! 예상 !! 실제"
local result_table_live_sandbox_header = "{|class=\"wikitable unit-tests-result\"\n|+ %s\n! !! 테스트 !! 현재 !! 연습장 !! 예상"
local result_table = { n = 0 }
local result_table_mt = {
insert = function (self, ...)
local n = self.n
for i = 1, select('#', ...) do
local val = select(i, ...)
if val ~= nil then
n = n + 1
self[n] = val
end
end
self.n = n
end,
insert_format = function (self, ...)
self:insert(string.format(...))
end,
concat = table.concat
}
result_table_mt.__index = result_table_mt
setmetatable(result_table, result_table_mt)
local num_failures = 0
local num_runs = 0
local function first_difference(s1, s2)
s1, s2 = tostring(s1), tostring(s2)
if s1 == s2 then return '' end
local max = math.min(#s1, #s2)
for i = 1, max do
if s1:sub(i,i) ~= s2:sub(i,i) then return i end
end
return max + 1
end
local function return_varargs(...)
return ...
end
function UnitTester:calculate_output(text, expected, actual, options)
-- Set up some variables for throughout for ease
num_runs = num_runs + 1
local options = options or {}
-- Fix any stripmarkers if asked to do so to prevent incorrect fails
local compared_expected = expected
local compared_actual = actual
if options.templatestyles then
local pattern = '(\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 fail
if expected_stripmarker_id then
compared_actual = compared_actual:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3') -- replace actual id with expected id; ignore second capture in pattern
compared_expected = compared_expected:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3') -- account for other strip markers
end
end
if options.stripmarker then
local pattern = '(\127[^\127]*UNIQ%-%-%l+%-)(%x+)(%-%-?QINU[^\127]*\127)'
local _, expected_stripmarker_id = compared_expected:match(pattern)
if expected_stripmarker_id then
compared_actual = compared_actual:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3')
compared_expected = compared_expected:gsub(pattern, '%1' .. expected_stripmarker_id .. '%3')
end
end
-- Perform the comparison
local success = compared_actual == compared_expected
if not success then
num_failures = num_failures + 1
end
-- Sort the wikitext for displaying the results
if options.combined then
-- We need 2 rows available for the expected and actual columns
-- Top one is parsed, bottom is unparsed
local differs_at = self.differs_at and (' \n| rowspan=2|' .. first_difference(compared_expected, compared_actual)) or ''
-- Local copies of tick/cross to allow for highlighting
local highlight = (should_highlight and not success and 'style="background:#fc0;" ') or ''
result_table:insert( -- Start output
'| ', highlight, 'rowspan=2|', success and tick or cross, -- 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)
local differs_at = self.differs_at and (' \n| ' .. first_difference(compared_expected, compared_actual)) or ''
local formatting = options.nowiki and mw.text.nowiki or return_varargs
local highlight = (should_highlight and not success and 'style="background:#fc0;"|') or ''
result_table:insert( -- Start output
'| ', highlight, success and tick or cross, -- Tick/Cross
' \n| ', mw.text.nowiki(text), ' \n| ', -- Text used for the test
formatting(expected), ' \n| ', formatting(actual), -- The formatted outputs
differs_at, -- Where any relevant difference was
'\n|-\n' -- End output
)
end
end
function UnitTester:preprocess_equals(text, expected, options)
local actual = frame:preprocess(text)
self:calculate_output(text, expected, actual, options)
end
function UnitTester:preprocess_equals_many(prefix, suffix, cases, options)
for _, case in ipairs(cases) do
self:preprocess_equals(prefix .. case[1] .. suffix, case[2], options)
end
end
function UnitTester:preprocess_equals_preprocess(text1, text2, options)
local actual = frame:preprocess(text1)
local expected = frame:preprocess(text2)
self:calculate_output(text1, expected, actual, options)
end
function UnitTester:preprocess_equals_compare(live, sandbox, expected, options)
local live_text = frame:preprocess(live)
local sandbox_text = frame:preprocess(sandbox)
local highlight_live = false
local highlight_sandbox = false
num_runs = num_runs + 1
if live_text == expected and sandbox_text == expected then
result_table:insert('| ', tick)
else
result_table:insert('| ', cross)
num_failures = num_failures + 1
if live_text ~= expected then
highlight_live = true
end
if sandbox_text ~= expected then
highlight_sandbox = true
end
end
local formatting = (options and options.nowiki and mw.text.nowiki) or return_varargs
local differs_at = self.differs_at and (' \n| ' .. first_difference(expected, live_text) or first_difference(expected, sandbox_text)) or ''
result_table:insert(
' \n| ',
mw.text.nowiki(live),
should_highlight and highlight_live and ' \n|style="background: #fc0;"| ' or ' \n| ',
formatting(live_text),
should_highlight and highlight_sandbox and ' \n|style="background: #fc0;"| ' or ' \n| ',
formatting(sandbox_text),
' \n| ',
formatting(expected),
differs_at,
"\n|-\n"
)
end
function UnitTester:preprocess_equals_preprocess_many(prefix1, suffix1, prefix2, suffix2, cases, options)
for _, case in ipairs(cases) do
self:preprocess_equals_preprocess(prefix1 .. case[1] .. suffix1, prefix2 .. (case[2] and case[2] or case[1]) .. suffix2, options)
end
end
function UnitTester:preprocess_equals_sandbox_many(module, function_name, cases, options)
local postfixes = {"/sandbox", "/연습장"}
for _, case in ipairs(cases) do
local live = module .. "|" .. function_name .. "|" .. case[1] .. "}}"
for _, postfix in ipairs(postfixes) do
local sandbox = module .. postfix .. "|" .. function_name .. "|" .. case[1] .. "}}"
self:preprocess_equals_compare(live, sandbox, case[2], options)
end
end
end
function UnitTester:equals(name, actual, expected, options)
num_runs = num_runs + 1
if actual == expected then
result_table:insert('| ', tick)
else
result_table:insert('| ', cross)
num_failures = num_failures + 1
end
local formatting = (options and options.nowiki and mw.text.nowiki) or return_varargs
local differs_at = self.differs_at and (' \n| ' .. first_difference(expected, actual)) or ''
local display = options and options.display or return_varargs
result_table:insert(' \n| ', name, ' \n| ',
formatting(tostring(display(expected))), ' \n| ',
formatting(tostring(display(actual))), differs_at, "\n|-\n")
end
local function deep_compare(t1, t2, ignore_mt)
local ty1 = type(t1)
local ty2 = type(t2)
if ty1 ~= ty2 then return false end
if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end
local mt = getmetatable(t1)
if not ignore_mt and mt and mt.__eq then return t1 == t2 end
for k1, v1 in pairs(t1) do
local v2 = t2[k1]
if v2 == nil or not deep_compare(v1, v2) then return false end
end
for k2, v2 in pairs(t2) do
local v1 = t1[k2]
if v1 == nil or not deep_compare(v1, v2) then return false end
end
return true
end
local function val_to_str(obj)
local function table_key_to_str(k)
if type(k) == 'string' and mw.ustring.match(k, '^[_%a][_%a%d]*$') then
return k
else
return '[' .. val_to_str(k) .. ']'
end
end
if type(obj) == "string" then
obj = mw.ustring.gsub(obj, "\n", "\\n")
if mw.ustring.match(mw.ustring.gsub(obj, '[^\'"]', ''), '^"+$') then
return "'" .. obj .. "'"
end
return '"' .. mw.ustring.gsub(obj, '"', '\\"' ) .. '"'
elseif type(obj) == "table" then
local result, checked = {}, {}
for k, v in ipairs(obj) do
table.insert(result, val_to_str(v))
checked[k] = true
end
for k, v in pairs(obj) do
if not checked[k] then
table.insert(result, table_key_to_str(k) .. '=' .. val_to_str(v))
end
end
return '{' .. table.concat(result, ',') .. '}'
else
return tostring(obj)
end
end
function UnitTester:equals_deep(name, actual, expected, options)
num_runs = num_runs + 1
if deep_compare(actual, expected) then
result_table:insert('| ', tick)
else
result_table:insert('| ', cross)
num_failures = num_failures + 1
end
local formatting = (options and options.nowiki and mw.text.nowiki) or return_varargs
local actual_str = val_to_str(actual)
local expected_str = val_to_str(expected)
local differs_at = self.differs_at and (' \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")
end
function UnitTester:iterate(examples, func)
require 'libraryUtil'.checkType('iterate', 1, examples, 'table')
if type(func) == 'string' then
func = self[func]
elseif type(func) ~= 'function' then
error(("bad argument #2 to 'iterate' (expected function or string, got %s)")
:format(type(func)), 2)
end
for i, example in ipairs(examples) do
if type(example) == 'table' then
func(self, unpack(example))
elseif type(example) == 'string' then
self:heading(example)
else
error(('bad example #%d (expected table, got %s)')
:format(i, type(example)), 2)
end
end
end
function UnitTester:heading(text)
result_table:insert_format(' ! colspan="%u" style="text-align: left" | %s \n |- \n ',
self.columns, text)
end
function UnitTester:run(frame_arg)
frame = frame_arg
self.frame = frame
self.differs_at = frame.args['differs_at']
tick = frame:preprocess('{{체크표}}')
cross = frame:preprocess('{{가위표}}')
local table_header = result_table_header
if frame.args['live_sandbox'] then
table_header = result_table_live_sandbox_header
end
if frame.args.highlight then
should_highlight = true
end
self.columns = 4
if self.differs_at then
table_header = table_header .. ' !! 값이 다르기 시작한 위치'
self.columns = self.columns + 1
end
-- Sort results into alphabetical order.
local self_sorted = {}
for key, _ in pairs(self) do
if key:find('^test') then
table.insert(self_sorted, key)
end
end
table.sort(self_sorted)
-- Add results to the results table.
for _, value in ipairs(self_sorted) do
result_table:insert_format("<h2>%s</h2>\n", value)
result_table:insert_format(table_header .. "\n|-\n", value)
self[value](self)
result_table:insert("|}\n")
end
return (num_runs == 0 and "<b>실행된 테스트가 없습니다.</b>"
or num_failures == 0 and "<b style=\"color:#008000\">모든 " .. num_runs .. "개의 테스트를 통과했습니다.</b>"
or "<b style=\"color:#800000\">" .. num_runs .. "개 중 " .. num_failures .. "개의 테스트를 통과하지 못했습니다.</b>"
) .. "\n\n" .. frame:preprocess(result_table:concat())
end
function UnitTester:new()
local o = {}
setmetatable(o, self)
self.__index = self
return o
end
local p = UnitTester:new()
function p.run_tests(frame) return p:run(frame) end
return p