自動化測試

Ren’Py 允許創作者在遊戲中加入自動化測試,以確保對遊戲的修改不會破壞已有功能;對大型遊戲或經常更新的遊戲尤其有用。

測試系統的兩個主要組成部分是 testcasetestsuite 語句。

renpy.is_in_test() 函數可用於判斷當前是否正在執行測試。

快速入門

本節內容通過一個最小化樣例,展示在實際測試用例中常用的語句:

  • run Jump("...") 從一個已知的腳本標籤開始運行。

  • advance until screen "choice" 自動前進直到出現某個分支選項菜單。

  • click "Choice Text" 選擇某個選項。

  • click id "..." until not screen "..." 不斷滑鼠點擊直到某個界面消失。.

首先,將下列程式碼加入到需要自動化測試的遊戲腳本中:

screen quickstart_popup():
    modal True

    frame:
        xalign 0.5
        yalign 0.5

        vbox:
            spacing 12
            text "快速入門彈出視窗"

            textbutton "關閉":
                id "quickstart_close"
                action Hide("quickstart_popup")


label quickstart_demo:
    "歡迎來到測試用例快速入門demo。"

    show screen quickstart_popup
    "關閉彈出視窗並繼續。"

    menu:
        "拿走地圖":
            "你拾取了地圖。"
        "離開":
            "你不管地圖並走開了。"

    "demo結束。"
    return

接著,添加測試用例(testcase)並執行上述代碼,驗證運行結果是否符合預期。

可以把測試用例跟前面的代碼放在同一個腳本文件中,也可以放在不同文件中。例如,新建一個名為 testcases.rpy 的文件放測試用例。

testsuite quick_start:
    before testcase:
        run Jump("quickstart_demo")
        advance until screen "quickstart_popup"

    testcase choose_map:
        pause 0.5
        click id "quickstart_close" until not screen "quickstart_popup"
        pause 0.5
        advance until screen "choice"
        click "拿走地圖"
        advance until "你拾取了地圖。"
        pause 0.5

    testcase leave_map:
        pause 0.5
        click id "quickstart_close" until not screen "quickstart_popup"
        pause 0.5
        advance until screen "choice"
        click "離開"
        advance until "你不管地圖並走開了。"
        pause 0.5

Note

demo中的pause語句不是必須的,它們只是能讓人看清測試過程。實際使用時完全可以刪除pause部分讓測試運行更快。

保存文件後,參考 運行測試用例 的內容在啟動器或命令行中運行測試用例。 如果選擇在命令行中運行測試,可以使用下列命令:

./renpy.sh /path/to/game test quick_start

運行測試用例

啟動器運行

如果從啟動器運行測試,選中需要測試的項目,然後點擊“Run Testcases”按鈕。 如果看不到這個按鈕,請嘗試下列操作:

  1. 確保遊戲項目中至少包含一個測試用例(testcase)。

  2. 點擊“啟動項目”按鈕正常啟動遊戲。

  3. 點擊啟動器的“刷新”按鈕。

默認情況下,這樣會運行“global”測試套件。

命令行運行

如果在命令行模式運行測試,就打開一個終端窗口並把路徑切換到Ren’Py的SDK目錄,然後使用 test命令

cd /path/to/renpy
./renpy.sh <basedir> test [<testcase>] [options...]
<basedir>

指定項目路徑。

<testcase>

指定運行的測試用例或測試套件。若未指定,默認運行“global”測試套件。

--enable_all

運行所有測試用例和測試套件,忽略它們的 enabled 設置。

--overwrite_screenshots

執行 screenshot語句 將覆蓋已存在的同名截圖圖片。

--hide-header

開始運行測試時禁用header部分。

--hide-execution {no|hooks|testcases|all}

隱藏測試執行的相關資訊。--hide-execution hooks 隱藏hook資訊,--hide-execution testcases 隱藏測試用例和hook,--hide-execution all 隱藏所有測試資訊。

--hide-summary

禁用測試運行完的總結。

--report-detailed

運行測試時顯示每項測試的詳細資訊。

--report-skipped

顯示跳過的測試的資訊。該選項應與 --report-detailed 一起使用。

testcase語句

testcase 語句用於創建一個具名測試用例,每個用例包含一塊測試語句(見下文)。 測試用例與 Ren’Py 的 label 類似,但有如下區別:

  • Ren’Py 的 label 語句包含 Ren’Py 代碼,而testcase語句包含的是本頁所列的測試語句,二者互斥。

  • testcase沒有與return語句等價的語句。

  • 測試語句不能出現在測試塊之外,而label之外可以有其他Ren’Py代碼。

testcase 語句可包含以下特性:

description

描述該測試用例的字串,會出現在測試報告中。

enabled

若該表達式結果為 False,則跳過該測試。預設值為 True

該特性可用於按條件禁用測試,例如在不支持的平台上。

testcase windows:
    enabled renpy.windows
    ...

testcase not_on_mobile:
    enabled not renpy.mobile
    ...

詳見 跳過測試用例

only

若該表達式結果為 True,則僅運行此測試用例(以及其它 only True 的測試)。預設值為 False

詳見 跳過測試用例

xfail

若該表達式結果為 True,表示預期該測試會失敗;若確實失敗,會將結果標記為xfailed而非failed。預設值為 False

parameter

變數名(或變數名元組)與取值列表(或元組列表)。測試會對列表中每個值(或每組元組)各運行一次。

一個測試用例可有多個 parameter 屬性,此時將針對所有取值組合各運行一次。

詳見 參數化測試

testsuite語句

testsuite 語句用於將測試用例分組。測試套件可包含測試用例、其它測試套件以及hooks(見一下)。

默認測試套件名為 global,若用戶未指定則由Ren’Py自動創建。其中包含遊戲中所有其它頂層測試套件和測試用例。

其特性與 testcase語句 相同。

hooks

testsuite 語句可包含以下hook:

setup

在當前套件內所有其他測試運行之前執行的test語句塊。

before testsuite

在當前套件內每個測試套件運行之前重複執行的test語句塊。

before testcase

在當前套件內每個測試用例運行之前重複執行的test語句塊。

after testcase

在當前套件內每個測試用例運行之後重複執行的test語句塊;即使測試用例失敗或拋出異常也會執行。

after testsuite

在當前套件內每個測試套件運行之後重複執行的test語句塊;即使測試套件失敗或拋出異常也會執行。

teardown

在當前套件內所有測試運行之後執行一次的test語句塊;即使有測試失敗或拋出異常也會執行。

before *after * 類型的hook可能包含以下特性:

depth

一個整數,用於指定hook應用深度。

對testcase,該特性預設值為 -1,表示作用於所有嵌套的測試套件和測試用例。

對testsuite,該特性預設值為 0,表示僅作用於當前套件直接包含的測試套件。

詳見 一次測試的生命週期

一次測試的生命週期

本節說明測試用例與測試套件的執行順序以及hook的調用方式。 以下範例用於說明:

代碼

執行順序

label test_demo:
    "This is a demo for testcases."
    "It has a few messages and a menu."
    menu:
        "First Choice":
            "Selected first choice."
        "Second Choice":
            "Selected second choice."
        "Third Choice":
            "Selected third choice."
    return

testsuite global:
    setup:
        $ print("global :: setup")
        skip until screen "main_menu"

    before testsuite:
        $ print("global :: before testsuite")
        if not screen "main_menu":
            run MainMenu(confirm=False)
        click "Start"

    before testcase:
        $ print("global :: before testcase")

    after testcase:
        $ print("global :: after testcase")

    after testsuite:
        $ print("global :: after testsuite")

    teardown:
        $ print("global :: teardown")
        exit

    testsuite basic:
        testcase first_testcase:
            $ print("basic :: first_testcase")
            advance

    testsuite test_choices:
        setup:
            $ print("test_choices :: setup")

        before testcase:
            $ print("test_choices :: before testcase")
            run Jump("test_demo")
            advance until screen "choice"

        after testcase:
            $ print("test_choices :: after testcase")

        teardown:
            $ print("test_choices :: teardown")

        testcase choice1:
            $ print("test_choices :: choice1")
            click "First Choice"

        testcase choice2:
            enabled False
            $ print("test_choices :: choice2 (disabled)")
            click "Second Choice"

        testcase choice3:
            $ print("test_choices :: choice3")
            click "Third Choice"

global :: setup

global :: before testsuite

global :: before testcase

basic :: first_testcase

global :: after testcase

global :: after testsuite

global :: before testsuite

test_choices :: setup

global :: before testcase

test_choices :: before testcase

test_choices :: choice1

test_choices :: after testcase

global :: after testcase

global :: before testcase

test_choices :: before testcase

test_choices :: choice3

test_choices :: after testcase

global :: after testcase

test_choices :: teardown

global :: after testsuite

global :: teardown

注意,即使測試用例位於嵌套的測試套件中, global :: before testcaseglobal :: after testcase 仍會在每個測試用例之前或之後執行。

要限制hook的作用範圍,可設置其 depth 特性。設為 0 時,hook僅對包含自身的測試套件內的測試生效。

例如:

testsuite global:
    before testcase:
        depth 0
        $ print("Starting a testcase.")

若要讓hook作用於嵌套的測試套件和測試用例,可將其 depth 設為 -1 (無限深度)或正整數(指定深度)。

Note

測試套件執行完畢後,遊戲不會自動關閉,而是將控制權交還玩家並等待輸入。

若要在測試套件結束後關閉遊戲,可在該測試套件的 after hook中使用 exit 測試語句。例如:

testsuite global:
    after:
        exit

跳過測試用例

若某測試用例被設置為跳過,則不會執行該測試;該測試套件的 before testcaseafter testcase 設置的hook也不會執行對應的測試用例。

若某測試套件內 所有 測試均被設置為跳過,則其 setupteardown hook也不會執行。 父測試套件的 before testsuiteafter testsuite 也不會執行。

參數化測試

通過 parameter 特性,可使一個測試用例使用不同參數多次運行。

方法為,指定變數名和對應的取值列表。測試時會使用列表中每個值各運行一次。例如:

testcase click_buttons:
    parameter button_name = ["Load", "Save"]
    click expression button_name

上述測試將運行兩次,先點擊“Load”按鈕,再點擊“Save”按鈕。

設計參數時需要考慮多個測試套件或測試用例的情況,以及對應的多個hook(包括 setupteardown)每次執行使用的值。

每次執行都會執行 before testcaseafter testcase hook,且測試報告中會分別列出運行結果。

參數分組

可將多個變數用括號分組,並給出多組取值列表,從而一次指定多個變數。例如:

testcase addition:
    parameter (x, y, z) = [ (1, 2, 3), (2, 3, 5), (3, 5, 8) ]

    assert eval (x + y == z)

將運行三次,每次使用一組值: ``(x=1, y=2, z=3)```(x=2, y=3, z=5)(x=3, y=5, z=8)

多個參數的組合

若存在多個 parameter 特性,測試將使用所有取值可能的組合各運行一次。例如:

testcase combinations:
    parameter a = [1, 2]
    parameter b = [3, 4]
    parameter c = [5, 6]

    assert eval (a + b + c in [9, 10, 11, 12])

將運行八次,對應 (a, b, c) 的所有組合。

可混合使用分組參數與單變數參數。例如:

testcase mixed:
    parameter a = [1, 2]
    parameter (b, c) = [ (3, 5), (4, 6) ]

    assert eval (a + b + c in [9, 10, 11, 12])

會運行四次,分別使用這些組合:

(1, (3, 5)), (1, (4, 6)), (2, (3, 5)), (2, (4, 6))

使用表達式中的參數

創作者可以在任何接受表達式的測試特性中使用參數。

例如,以下測試會運行三次,每次使用不同的 x 值。 當 x 為0或1時測試通過,當 x 為2時預計失敗(xfail):

testcase choice_test:
    parameter x = [0, 1, 2]
    xfail x == 2

    assert eval (x < 2)

也可使用參數名稱來選擇界面或按鈕。例如以下測試會根據 choice_text 的值點擊“first”或“second”選項:

testcase show_menu:
    parameter screen_name = ["preferences", "load"]

    $ print(f"Showing screen '{screen_name}'")
    run ShowMenu(screen_name)
    pause until screen screen_name
    run Return()

參數可以跟在 expression 後面,用於根據參數名選擇一個按鈕。

testcase click_buttons:
    parameter button_name = ["Load", "Save"]

    click expression button_name

參數也可在Python代碼塊中使用。例如,以下測試會列印當前 xy 的值,並以此作為坐標並點擊對應位置:

testcase param_test:
    parameter (x, y) = [(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)]

    $ print(f"Clicking at position ({x}, {y})")
    click pos (x, y)

參數化的測試套件

測試參數也可以直接應用於整個測試套件。此時套件內所有hook和測試用例會使用每組參數各運行一次。

每次參數化運行都會執行 setupbefore/after testsuiteteardown hook。

例如:

testsuite math_tests:
    parameter (x, y, z) [ (1, 2, 3), (2, 3, 5), (3, 5, 8) ]

    setup:
        $ print(f"Running math tests with x={x}, y={y}, z={z}")

    testcase addition:
        assert eval (x + y == z)

    testcase multiplication:
        assert eval (x*y == z*y - y*y)

參數可以嵌套,所有參數組合都會被測試。例如:

testsuite parameter_field:
    parameter choice_text = ["first", "second"]

    testcase param_test2:
        parameter (x, y) = [(0.0, 0.0), (0.5, 0.5)]

        advance until screen "choice"
        click choice_text
        click pos (x, y)

Warning

參數按引用傳遞。若修改可變參數(例如列表或字典),則共享同一對象的其他測試也會受到影響。

異常和失敗處理

若在運行測試用例期間發生錯誤:

  1. 該測試用例立即停止執行。

  2. 包含該測試用例的測試套件的 after testcase hook 會執行。

  3. 若還有其它測試用例,會繼續執行其他測試用例(包括 before testcase hook)。

  4. 若沒有更多測試用例,則執行該測試套件的 after hook。

若在hook執行期間發生錯誤(例如 before testcase):

  1. 該測試套件立即停止執行。

  2. 若該套件由另一套件調用,父套件會繼續執行。

  3. 若不存在父套件,遊戲將結束測試。

測試報告

測試結束後,將結果輸出到控制台,列出所有測試用例及其結果。若啟用了 --print_details 選項,報告中會包含每項測試的詳細資訊。

以下是成功測試“The Question”後的測試報告範例:

Test report example

測試結果

一項測試可能得到的結果如下:

  • Passed:測試成功,沒有任何錯誤。

  • Failed:已執行測試,但某條語句失敗。

  • XFailed:測試按預期失敗(其 xfailTrue 且確實失敗)。

  • XPassed:測試預期失敗(xfailTrue),卻通過測試。

  • Skipped:測試被跳過(enabledFalse 或存在 only True 的其它測試)。

通常,passed或xfailed視為成功,failed或xpassed視為不成功。

測試設置

以下配置項可用於修改測試行為:

_test.maximum_framerate

該項是一個布林值。用於指定是否在測試時使用最大幀率模式。解鎖後幀率可能會超過顯示器刷新率。 預設值為 True

_test.timeout

該項是一個浮點數。用於指定單條測試語句等待條件滿足的最長時間(秒)。預設值為 10.0

可在支持 timeout 的語句(如 assertuntil)上單獨指定超時時間並覆蓋該值。

_test.force

該項是一個布林值。用於指定是否在 renpy.config.suppress_underlayTrue 時仍強制繼續測試。預設值為 False

_test.transition_timeout

該項是一個浮點數。用於指定等待轉場完成的最長時間,單位為秒。若超時則跳過轉場繼續測試。預設值為 5.0

_test.focus_trials

該項是一個整數。用於指定在使用未指定坐標的選擇器時,測試系統嘗試為 移動滑鼠 尋找有效坐標的次數。預設值為 100

_test.screenshot_directory

該項是一個字串。用於指定截圖保存目錄。預設值為 tests/screenshots

_test.vc_revision

該項是一個字串。用於指定當前原始碼樹的版本控制(多為git)修訂號。 預設值為 環境變數 RENPY_TEST_VC_REVISION 的值,未設置則為空字串。

測試語句

測試語句是測試用例的基礎結構,大致分為三類:命令語句、條件/選擇器語句、控制語句。

基本命令

advance語句

Type: Command

advance

使遊戲推進到下一行對話。

advance
advance until screen "choice"

exit語句

Type: Command

exit

不彈出確認界面直接退出遊戲,退出前不存檔。

if eval need_to_confirm:
    # 彈出確認界面,若 config.autosave_on_quit 為 True 則自動存檔。
    run Quit(confirm=True)

if eval persistent.quit_test_using_action:
    # 不彈出確認界面,若 config.autosave_on_quit 為 True 則自動存檔。
    run Quit(confirm=False)

exit # 不彈出確認界面,退出前不存檔。

pass語句

Type: Command

pass

不執行任何操作。占位用,用於空測試用例。

testcase not_yet_implemented:
    pass

pause語句

Type: Command

pause [time (float)]

根據指定的時間暫停測試。暫停持續時間單位為秒。與 pause語句 類似,但必須給出時間。作為 until 子句時可以不寫時間。

pause 5.0
pause until screen "inventory"

run語句

Type: Command

run <action>

執行指定的 界面行為 (或行為列表)。

當包含該行為(或行為列表)的按鈕變為可點擊時繼續。

testcase chapter_3:
    run Jump("chapter_3")

skip語句

Type: Command

skip [fast]

使遊戲開始快進。若當前在菜單中則先返回遊戲,否則僅開啟快進。

If fast is provided, the game will skip directly to the next menu choice.

若啟用 fast,將直接快進到下一個選項。

skip
skip fast
skip until screen "choice"

滑鼠命令

click語句

Type: Command

click [button (int)] [selector] [expression (expr)] [pos (x, y)]

在螢幕上執行模擬點擊。可選特性如下:

  • button 指定模擬滑鼠的哪個按鍵。該項是一個整數,預設值為1。 1為左鍵,2為右鍵,3為中鍵,4和5為部分滑鼠的額外按鍵。Ren’Py通常只響應 1 和 2。

  • expression 指定一個測試表達式,用於標記點擊目標。

    其遵守 pattern使用從句的規則。

若指定了 selector 和/或 pos,會先按 move語句 的規則移動虛擬測試滑鼠再發送點擊。

使用 通配符 子句的點擊行為,若未指定 pos,click語句會尋找“中性”位置,使點擊不會落在可獲得焦點的元素上。

# 點擊滑鼠當前位置。
click

# 點擊帶有指定文本的按鈕。
click "Start"

# 滑鼠右鍵點擊指定目標。
click id "inventory_button" button 2

# 點擊指定目標中心。
click id "inventory_button" pos (0.5, 0.5)

# 點擊使用表達式指定的目標。
$ button_name = "Load"
click expression button_name

Note

若要推進對話,請使用 advance語句skip語句。 使用click語句可能會因滑鼠位置和當前畫面無法確定最終結果。

drag語句

Type: Command

drag <[selector] [pos (x, y)]> to <[selector] [pos (x, y)]> [button (int)] [steps (int)]

模擬螢幕上的滑鼠拖拽。可選特性如下:

  • 拖拽起點(to 之前),可使用 selector 和/或 pos 特性,與 move語句 的規則相同。

  • 拖拽終點(to 之後),同樣可使用 selector 和/或 pos 特性,與 move語句 的規則相同。

  • button 指定拖拽使用的滑鼠按鍵。該項是一個整數,預設值為1。

  • steps 指定拖拽中間步數。該項是一個整數,預設值為 10。步數越多越平滑但更耗時。

drag id "item_icon" to id "inventory_slot_3" button 1 steps 20
drag pos (100, 200) to pos (400, 500) button 1
drag id "item_icon" pos (0.5, 0.5) to pos (300, 400) steps 5
drag pos (50, 50) to id "inventory_slot_1"
drag pos (50, 50) to pos (150, 150)

move語句

Type: Command

move [selector] [pos (x, y)]

將虛擬測試滑鼠移動到螢幕上的指定位置。

If a selector is given, and:

若指定了 selector

  • 若指定 pos,該項是一個相對於選擇器的坐標,滑鼠移動對應坐標。

  • 若未指定 pos,會嘗試找到點擊後可使該選擇器獲得焦點的像素。該項也會參考其他東西,比如 focus_mask 等。

If no selector is given, and:

若未指定 selector

  • 若指定 pos,該項是一個相對於螢幕的坐標,滑鼠移動對應坐標。

  • 若未指定 pos,會拋出錯誤。

# 移動到 `back_btn` 內的隨機可點擊點
move id "back_btn"

# 移動到 `back_btn` 的中心點
move id "back_btn" pos (0.5, 0.5)

# 移動到距離 `back_btn` 的左上角右側20像素下方10像素的位置。
move id "back_btn" pos (20, 10)

# 移動到螢幕的右上角
move pos (1.0, 0.0)

# 移動到距離螢幕的左上角右側20像素下方10像素的位置。
move pos (20, 10)

scroll語句

Type: Command

scroll [amount (int)] [selector] [pos (x, y)]

Simulates a scroll event. It takes the following optional properties:

模擬滾動事件。可選特性如下:

  • amount 表示滾動“格數”。該項是一個整數,預設值為 1。正數向下滾動,負數向上滾動。

  • 若指定了 selector 和/或 pos,會先執行 move語句 移動滑鼠再發送滾動。

    scroll “bar” scroll id “inventory_scroll” scroll amount 10 id “inventory_scroll” pos (0.5, 0.5) scroll # 向下滾動到當前滑鼠位置

Note

僅模擬滑鼠滾輪事件;也可考慮使用 界面行為(action)、值(value)和函數 中的Scroll行為。

run Scroll("inventory_scroll", "increase", amount="step", delay=1.0)

鍵盤命令

keysym語句

Type: Command

keysym <keysym> [selector] [pos (x, y)]

模擬鍵盤按鍵事件,包括 config.keymap 中的鍵位。

若指定了 selector 和/或 pos,會先執行 move 語句 移動滑鼠再發送按鍵命令。

keysym "skip"
keysym "help"
keysym "ctrl_K_a"
keysym "K_BACKSPACE" repeat 30
keysym "pad_a_press"

type語句

Type: Command

type <string> [selector] [pos (x, y)]

將給定字串模擬鍵盤方式輸入。

若指定了 selector 和/或 pos,會先執行 move 語句 移動滑鼠再發送文本。

type "Hello, World!"

條件語句

條件語句用於判斷某前提條件是否成立。用於接受條件的測試語句有 ifassertuntil

布林值

測試中可根據需要使用布林值 TrueFalse。這兩個值可以直接使用。

if True:
    click "Start"

if False:
    click "Settings" # 不會執行這條,因為判斷條件始終為False。

布爾運算

條件語句支持 notandor 運算符。表達式外層可加括號也可不加。

assert eval (renpy.is_in_test() and screen "main_menu")
advance until "ask her right" or label "chapter_five"
click "Next" until not screen "choice"

eval語句

Type: Condition

eval <expression>

根據給定的Python表達式計算結果。僅用於 assertifuntil 等接受條件的測試語句內部。

assert eval (renpy.is_in_test() and ("Ren'Py" in renpy.version_string))

Note

帶$符號的單行Python語句與eval語句的區別:

  • eval 不能單獨成行,必須放在 ifuntil 等語句內;帶$符號的單行Python語句是單獨一行。

  • 帶$符號的單行Python語句可執行任意Python語句(不必有返回值,如 $ import math);eval語句必須有返回值。

label語句

Type: Condition

label <labelname>

檢查上一條執行的測試語句是否已到達指定的Ren’Py腳本標籤。

參考一下例:

run Jump("chapter_1")
assert label chapter_1 # 成功
assert label chapter_1 # 失敗

第一條 assert 語句成功是因為上一條語句 run Jump("chapter_1") 正好到達指定腳本標籤 chapter_1。 第二條 assert 失敗是因為自第一條 assert 語句之後,未再次到達指定腳本標籤 chapter_1

因此下例不會得到成功的結果:

run Jump("chapter_1")
advance repeat 3
assert label chapter_1 # 失敗

Warning

測試用label語句不應與Ren’Py原聲 label語句 或界面中的 label元素 混淆。

(譯者註:測試label語句應該只在具有分支選項的測試後面使用,用於確認分支走向符合預設劇本。)

選擇器語句

選擇器語句用於判斷某元素是否顯示在當前螢幕上,並用於後續操作。

選擇器是一類特殊的條件語句。

可視組件選擇器

Type: Condition, Selector

檢查指定id的界面或元素是否當前已顯示。

其可使用以下特性:

screen <name>

要檢查的界面名稱。

id

要檢查的元素id。

layer

界面所在圖層。未指定圖層時根據界面名稱自動確定。

if screen "main_menu":
    click "Start"

advance until id "inventory_viewport" layer "overlay"

click "Close" until not id "close_button"

文本選擇器

Type: Condition, Selector

"<text>" [raw]
expression <expression> [raw]

text 選擇器接受一個字串,表示需要在螢幕上尋找的目標。 尋找方式是遍歷所有可獲得焦點的元素(通常是按鈕和主對話文本框),尋找它們自身的文本和 alt 特性文本。

搜索時不區分大小寫且取最短匹配。 例如,搜索 "log" 且螢幕裡有 "CATALOG""illogical" 時,會匹配到 "CATALOG"

若指定了 raw,在腳本原文內搜尋,在多語言處理與 插值 之前。 否則在螢幕上顯示的文字上搜尋,在多語言處理與 插值 之後。

若指定了 expression,則會根據表達式計算結果字串決定待搜索目標。

# 這段文字可能會出現在按鈕上
skip until "Start Game"

# 這段文字可能會出現在主對話文本框中
advance until "Hey, that's not fair!"

# 不區分大小寫的搜索
assert "AsK HeR RighT AwaY"

# 搜尋未插值的文本
assert "Welcome, Eileen!"
assert "Welcome, [player_name]!" raw

# 在修改語言後,搜尋未經多語言處理的文本
run Language("japanese")
assert "スタート"
assert "Start" raw

控制語句

控制語句用於控制測試執行流程。

assert語句

Type: Control

assert <condition> [timeout (float)] [xfail (bool)]

該語句接受一個條件語句,執行時遇到前提條件不滿足則拋出 enpyTestAssertionError。

若指定了 timeout,等待對應秒數就是前提條件;如果前提條件超時則該語句執行失敗。

xfail 設置為 True,表示預期assert語句執行失敗。 這樣該語句是否成功與條件是否滿足的結果相反。如果前提條件滿足,該語句執行失敗;反之則成功。

assert screen "main_menu"
assert eval some_function(args)
assert id "start_button" timeout 5.0

for語句

Type: Control

for <variable> in <iterable>

該語句會對所提供可迭代對象中的每一項執行一段test語句塊。 使用 breakcontinue 語句可以控制循環流程。

樣例:

# 點擊“Next”三次。
for _ in range(3):
    click "Next"

# 依次點擊每一個選項。如果商店未解鎖則跳過點擊“Trade”選項。
for choice in ["Talk", "Trade", "Leave"]:
    if eval (choice == "Trade" and not persistent.shop_unlocked):
        continue
    click expression choice
    if screen "shop":
        break

# 點擊stats界面的每一個標籤。如果任務系統未解鎖則跳過點擊“Quests”標籤。
for stat_tab in ["Stats", "Skills", "Quests"]:
    click expression stat_tab
    if eval (stat_tab == "Quests" and not persistent.quests_enabled):
        continue
    assert text stat_tab timeout 2.0

if語句

Type: Control

if <condition>

當指定條件滿足時執行其內部的測試語句塊。

樣例:

if label "chapter_five":
    exit

if eval (persistent.should_advance and i_should_advance["now"]):
    advance

elifelse 語句可為 if 語句添加分支。

if eval persistent.should_advance:
    advance
elif eval i_should_advance["now"]:
    advance
else:
    click "Start"

repeat語句

Type: Control

<command> repeat <number> [timeout (float)]

將某條語句重複執行指定次數。 repeat 左側的內容是需要執行的語句,右側是重複次數。

click "+" repeat 3
keysym "K_BACKSPACE" repeat 10
advance repeat 3

screenshot語句

Type: Command

screenshot <path> [max_pixel_difference (int or float)] [crop (x, y, width, height)]

對當前畫面截圖並保存到指定路徑。

  • path:保存路徑(與 _test.screenshot_directory 的相對路徑),可含副檔名,僅支持 .png

  • max_pixel_difference:與已有截圖允許的像素差異數(整數)或比例(浮點),預設為 0

  • crop:裁剪區域 (x, y, width, height),坐標必須為整數。

若項目在git倉庫中,當前提交的哈希會自動追加到檔案名(如 @{hash}.png),便於開發者追蹤截圖變化。

若文件已存在,會將當前截圖與已有文件比較。差異超過 max_pixel_difference 設置的像質數會拋出RenpyTestScreenshotError。

若要覆蓋已有截圖,可刪除文件或在運行測試時使用 --overwrite_screenshots 命令行選項。

screenshot "screens/main_menu.png"
screenshot "screens/inventory" max_pixel_difference 0.01
screenshot "button.png" crop (10, 10, 100, 50)

參數化測試中可以使用多張截圖:

testcase screen_tester:
    parameter screen_name = ["inventory", "stats", "map"]

    run Show(screen_name)
    screenshot f"screens/{screen_name}.png"

until語句

Type: Control

<command> until <condition> [timeout (float)]

重複執行某個語句直到滿足預設條件。由 until 連接左右兩側,左側是重複執行的語句,右側是條件。

當右側條件滿足時進入下一條語句。否則不斷重複執行左側語句直到條件滿足。

若指定了 timeout,會在對應時間內等待條件滿足,單位為秒。 如果超時仍未滿足則拋出RenpyTestTimeoutError。

該超時會暫時覆蓋全局 _test.timeout

advance until screen "choice"
click "Next"
advance until label "chapter_5"

skip until screen "inventory" timeout 20.0

while語句

Type: Control

while <condition>

該語句會在提供的條件(condition)滿足時不斷執行一段test語句塊。 使用 breakcontinue 語句可以控制循環流程。

樣例:

$ should_advance = True
while eval should_advance:
    advance
    if screen "main_menu":
        break

    $ should_advance = some_evaluation_function()

Python語句塊和帶$符號的單行語句

測試用例內可加入 python語句塊帶$符號的單行語句。 與普通Ren’Py代碼不同,這裡的Python語句塊不接受 in substore 參數,但可使用 hide 關鍵字。 二者都可執行任意Python語句。

init代碼會在測試運行前執行,因此 init python 中定義的函數和類可在測試的Python語句塊和帶$符號的單行語句中調用。例如:

init python in test:
    def afunction():
        if renpy.is_in_test():
            return "test"
        return "not test"

testcase default:
    $ print(test.afunction()) # 在控制台輸出結果