모듈: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_at1이 주어진 경우, 기댓값과 실젯값이 다르기 시작한 첫번째 위치를 나타내는 열이 추가됩니다. 일반적으로 유닛 테스트 코드가 있는 모듈의 토론 문서(모듈토론)에서 사용됩니다.

    {{ #invoke: Bananas/시험장 | run_tests }}
    {{ #invoke: Bananas/시험장 | run_tests | differs_at=1 }}

preprocess_equals[편집]

  • preprocess_equals(text, expected)
  • preprocess_equals(text, expected, options)

전처리 과정을 거쳐 실젯값을 출력해줄 위키텍스트와 평문으로 작성된 기댓값을 각각 매개변수 textexpected로 받은 뒤, 전처리를 실행한 값과 기댓값이 같은지 비교하여 테스트의 성공 여부를 판별합니다. 스크립트와 틀은 문서에서 불러오는 방식과 동일한 방식으로 불러옵니다.

    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에 대입하며 반복 실행시킵니다. 이 때 매개변수 prefixsuffix가 위키텍스트 앞뒤에 각각 접두/접미 문자열로써 자동으로 추가됩니다.

    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)

전처리 과정을 거쳐 실젯값을 출력해줄 위키텍스트 및 같은 방식으로 기댓값을 출력해줄 위키텍스트를 각각 매개변수 textexpected로 받은 뒤, 두 위키텍스트의 처리 결과가 같은지 비교하여 테스트의 성공 여부를 판별합니다. 스크립트의 출력값과 위키백과에 먼저 있었던 틀의 출력값이 서로 일치하는지 확인하고자 할 때 유용합니다.

    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에 대입하며 반복 실행시킵니다. 이 때 매개변수 prefix1suffix1은 첫번째 부분(실젯값을 출력해줄 위키텍스트)의 앞뒤에, 매개변수 prefix2suffix2은 두번째 부분(기댓값을 출력해줄 위키텍스트)의 앞뒤에 각각 접두/접미 문자열로써 자동으로 추가됩니다. 테스트 케이스에서 두번째 부분(기댓값을 출력해줄 위키텍스트)이 따로 기재되지 않은 경우엔 기대값을 출력해줄 위키텍스트로써 첫번째 부분(실젯값을 출력해줄 위키텍스트)을 그대로 사용합니다.

    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)

계산된 값과 기댓값을 각각 매개변수 actualexpected로 받아서 == 연산자를 이용해 둘이 일치하는지 확인합니다. 다른 모듈에 쓰이도록 설계된 모듈을 테스트할 때 #invoke를 사용하는 것보다 유용합니다.

    self:equals('Simple addition', 2 + 2, 4, { nowiki=1 })

equals_deep[편집]

  • equals_deep(name, actual, expected)
  • equals_deep(name, actual, expected, options)

equals와 비슷하지만 매개변수 actualexpected로 주어진 루아 테이블을 심층적인 비교를 하는 방식을 이용함으로써 서로 같은지 비교합니다. 이 때 어떤 값도 순환 참조를 포함해선 안되는데, 이는 현재의 구현상태에선 다루고 있지 않아 무한 루프에 빠질 수 있기 때문입니다.

    self:equals_deep('Table comparison', createRange(1,3), {1,2,3}, { nowiki=1 })

같이 보기[편집]

-- UnitTester는 다른 루아 스크립트에 사용할 수 있는 유닛 테스트 도구를
-- 제공합니다. 자세한 것은 [[위키백과:루아#유닛 테스트]]를 참조하세요.
-- 사용자를 위한 설명서는 상단의 설명문서를 참고해주세요.
local UnitTester = {}

local frame, tick, cross
local result_table_header = "{|class=\"wikitable\"\n! !! 테스트 구문 !! 기댓값 !! 실젯값"
local result_table = ''
local num_failures = 0

function first_difference(s1, 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

function UnitTester:preprocess_equals(text, expected, options)
    local actual = frame:preprocess(text)
    if actual == expected then
        result_table = result_table .. '| ' .. tick
    else
        result_table = result_table .. '| ' .. cross
        num_failures = num_failures + 1
    end
    local maybe_nowiki = (options and options.nowiki) and mw.text.nowiki or function(...) return ... end
    local differs_at = self.differs_at and (' \n| ' .. first_difference(expected, actual)) or ''
    result_table = result_table .. ' \n| ' .. mw.text.nowiki(text) .. ' \n| ' .. maybe_nowiki(expected) .. ' \n| ' .. maybe_nowiki(actual) .. differs_at .. "\n|-\n"
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)
    if actual == expected then
        result_table = result_table .. '| ' .. tick
    else
        result_table = result_table .. '| ' .. cross
        num_failures = num_failures + 1
    end
    local maybe_nowiki = (options and options.nowiki) and mw.text.nowiki or function(...) return ... end
    local differs_at = self.differs_at and (' \n| ' .. first_difference(expected, actual)) or ''
    result_table = result_table .. ' \n| ' .. mw.text.nowiki(text1) .. ' \n| ' .. maybe_nowiki(expected) .. ' \n| ' .. maybe_nowiki(actual) .. 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:equals(name, actual, expected, options)
    if actual == expected then
        result_table = result_table .. '| ' .. tick
    else
        result_table = result_table .. '| ' .. cross
        num_failures = num_failures + 1
    end
    local maybe_nowiki = (options and options.nowiki) and mw.text.nowiki or function(...) return ... end
    local differs_at = self.differs_at and (' \n| ' .. first_difference(expected, actual)) or ''
    result_table = result_table .. ' \n| ' .. name .. ' \n| ' .. maybe_nowiki(tostring(expected)) .. ' \n| ' .. maybe_nowiki(tostring(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

function val_to_str(v)
    if type(v) == 'string' then
        v = mw.ustring.gsub(v, '\n', '\\n')
        if mw.ustring.match(mw.ustring.gsub(v, '[^\'"]', ''), '^"+$') then
            return "'" .. v .. "'"
        end
        return '"' .. mw.ustring.gsub(v, '"', '\\"' ) .. '"'
    else
        return type(v) == 'table' and table_to_str(v) or tostring(v)
    end
end

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

function table_to_str(tbl)
    local result, done = {}, {}
    for k, v in ipairs(tbl) do
        table.insert(result, val_to_str(v))
        done[k] = true
    end
    for k, v in pairs(tbl) do
        if not done[k] then
            table.insert(result, table_key_to_str(k) .. '=' .. val_to_str(v))
        end
    end
    return '{' .. table.concat(result, ',') .. '}'
end

function UnitTester:equals_deep(name, actual, expected, options)
    if deep_compare(actual, expected) then
        result_table = result_table .. '| ' .. tick
    else
        result_table = result_table .. '| ' .. cross
        num_failures = num_failures + 1
    end
    local maybe_nowiki = (options and options.nowiki) and mw.text.nowiki or function(...) return ... end
    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 = result_table .. ' \n| ' .. name .. ' \n| ' .. maybe_nowiki(expected_str) .. ' \n| ' .. maybe_nowiki(actual_str) .. differs_at .. "\n|-\n"
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 self.differs_at then
        table_header = table_header .. ' !! 값이 다르기 시작한 위치'
    end

    -- ABC, 가나다순으로 결과 정렬
    local self_sorted = {}
    for key,value in pairs(self) do
        if key:find('^test') then
            table.insert(self_sorted, key)
        end
    end
    table.sort(self_sorted)
	-- 결과 표에 결과들을 추가
    for i,value in ipairs(self_sorted) do
        result_table = result_table .. "'''" .. value .. "''':\n" .. table_header .. "\n|-\n"
        self[value](self)
        result_table = result_table .. "|}\n\n"
    end

    return (num_failures == 0 and "<span style=\"color:green\;font-weight:bold;\">모든 테스트를 통과했습니다.</span>" or "<span style=\"color:maroon;font-weight:bold;\">" .. num_failures .. "개의 테스트를 통과하지 못했습니다.</span>") .. "\n\n" .. frame:preprocess(result_table)
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