Robot Framework: 擴充現有的 Test Library

官方的 User Guide 提到,BuiltIn Library 是擴充現有 test library 最好的方法。下面以擴充 Screenshot Library 為例:

examples/extkw/test.txt

| *Setting* | *Value*
| Library | Screenshot
| Library | MyScreenshotLibrary

| *Test Case* | *Action* | *Argument*
| Test | Take Snapshot | desktop
Caution 雖然沒有直接用到 Screenshot Library,但如果沒有明確引進來的話,執行期會出現 “No library with name ‘Screenshot’ found." 的錯誤,建議在 test library 的文件上額外做說明。

examples/extkw/MyScreenshotLibrary

from robot.libraries.BuiltIn import BuiltIn

class MyScreenshotLibrary:

  _builtin = BuiltIn()

  @property
  def _screenshot_lib(self):
    return self._builtin.get_library_instance('Screenshot') 1

  def take_snapshot(self, name, width='800px'):
      img = self._screenshot_lib.take_screenshot(name, width) 2
      print '*WARN* snapshot:', img
1 透過 BuiltIn instance 取得其他 library 的 active instance,跟 test library 的 scope 無關。
2 等同在 test data 裡 Screenshot.Take Screenshot 的用法。

執行結果:

$ pybot -d /tmp test.txt
==============================================================================
Test
==============================================================================
[ WARN ] snapshot: /tmp/desktop_1.jpg
Test                                                                  | PASS |
Tip

BuiltIn Library 是 global scope,我們手動生成的那個 BuiltIn instance 當然跟 runtime 系統唯一的那個 BuiltIn Library instance 不同,但我們卻可以透過它去取得其他 library instance?

那是因為 test library instance 並不是記錄在 BuiltIn Library instance 裡:

libraries/BuiltIn.py

from robot.running import Keyword, NAMESPACES, RUN_KW_REGISTER

class _Misc:

    def get_library_instance(self, name):
        try:
            return self._namespace.get_library_instance(name)
        except DataError, err:
            raise RuntimeError(unicode(err))

class BuiltIn(_Verify, _Converter, _Variables, _RunKeyword, _Misc):
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'

    @property
    def _namespace(self):
        return NAMESPACES.current

running/__init__.py

class _Namespaces:

# Hook to namespaces
NAMESPACES = _Namespaces() 1
1 原來 NAMESPACES 是個 singleton。

或許 BuiltIn.get_library_instance() 一開始就被設計成 static/class method 就不會有使用上的疑慮了。

如果這是改變不了的事實,建議將 BuiltIn instance 快取起來,這樣取用 test case 或 test suite scope 的 library instance 時,就不用一直生成全新的 BuiltIn instance。

下面示範擴充 Selenium Library 的做法:

MySeleniumLibrary.py

class MySeleniumLibrary:

    _builtin = BuiltIn()
    _selenium_lib = None

    @property
    def _screenshot_lib(self):
        return self._builtin.get_library_instance('Screenshot') # test suite scope

    @property
    def _selenium(self):
        if self._selenium_lib is None:
            self._selenium_lib = self._builtin.get_library_instance('SeleniumLibrary')
        return self._selenium_lib._selenium

    def login(self, username, password, language=None, service_hint=None):
        self._selenium.open(...)
        self._screenshot_lib.take_screenshot()
Warning

get_library_instance() 不能在 __init() 裡呼叫,否則 RIDE 和真正跑測試時,會分別出現下面的錯誤:(事實上任何初始化的工作都不應該在 __init__() 裡做)

AttributeError: 'NoneType' object has no attribute 'get_library_instance'
[ ERROR ] Invalid syntax in file 'xxx.txt' in table 'Settings': Creating an instance of the test library 'MyLibrary' with no arguments failed: No library with name 'SeleniumLibrary' found.

顯然我們不能假設 Robot Framework 載入 test libraries 的順序…

Note

參考資料

廣告

Robot Framework: 實作 Run Keyword If XXX

首先用一個例子來說明為什麼會有自訂 Run Keyword If XXX 的需求。假設產品測試人員用下面的 test case 在英文環境下測試某些 wording:

| *Test Case* | *Action* | *Argument* | *Argument*
| Test        | Log | Open the main screen. | WARN
|             | Log | Verify English wording | WARN
|             | Log | Click Configure button to open Perferences window. | WARN
|             | Log | Verify another English wording | WARN

之後 L10N team 會做一點加工:

| *Test Case* | *Action* | *Argument* | *Argument*
| Test        | Log | Open the main screen. | WARN
|             | Log | Take screenshot for L10N verficiation. | WARN 1
|             | Log | Verify English wording | WARN
|             | Log | Click Configure button to open Perferences window. | WARN
|             | Log | Take screenshot for L10N verficiation. | WARN
|             | Log | Verify another English wording | WARN
1 在適當的地方安插畫面截圖的動作,事後能以人工的方式快速校閱翻譯的結果。

但這個加工後的 test case 無法在非英文的環境下通過測試,因為 “Verify another English wording" 的動作一定失敗。

為了讓 L10N team 可以直接延用產品測試人員所寫的 test case,必須設計一個機制使能夠在進行 L10N 測試時略過某些檢查,但又不影響原有的功能測試。

examples/runkw/test.txt

| *Setting* | *Value*
| Library | L10NLibrary.py

| *Test Case* | *Action* | *Argument* | *Argument*
| Test        | Log | Open the main screen. | WARN
|             | Log | Take screenshot for L10N verficiation. (1) | WARN
|             | Run Keyword If Not L10N | Log | Verify English wording | WARN 1
|             | Log | Click Configure button to open Perferences window. | WARN
|             | Log | Take screenshot for L10N verficiation. (2) | WARN
|             | ${L10N}= | Get Variable Value | ${L10N} | not-defined
|             | Run Keyword If | '${L10N}' == 'not-defined' | Log | Verify another English wording | WARN 2
1 Run Keyword If Not L10N 應運而生,這一行在做 L10N 測試時沒有作用。
2 這裡示範了如果沒有 Run Keyword If Not L10N,自己用 Run Keyword If 判斷會有點小麻煩。

當然我們也可以規劃另一個 Run Keyword If L10N 來控制某些步驟只有在做 L10N 測試時才會有作用;但就這個例子而言,用 Take Screenshot If L10N 會比較洽當。

examples/runkw/L10NLibrary.py

from robot.libraries.BuiltIn import BuiltIn

_builtin = BuiltIn()

class L10NLibrary:

  def run_keyword_if_not_l10n(self, name, *args):
    l10n = _builtin.get_variable_value('${L10N}') 1
    if l10n is None: _builtin.run_keyword(name, *args)
1 L10N 這個變數來控制 L10N 測試時所關注的語言。

測試結果:

$ pybot test.txt
==============================================================================
Test
==============================================================================
[ WARN ] Open the main screen.
[ WARN ] Take screenshot for L10N verficiation. (1)
[ WARN ] Verify English wording
[ WARN ] Click Configure button to open Perferences window.
[ WARN ] Take screenshot for L10N verficiation. (2)
[ WARN ] Verify another English wording
Test                                                                  | PASS |
...

$ pybot --variable L10N:zh-tw test.txt
==============================================================================
Test
==============================================================================
[ WARN ] Open the main screen.
[ WARN ] Take screenshot for L10N verficiation. (1)
[ WARN ] Click Configure button to open Perferences window.
[ WARN ] Take screenshot for L10N verficiation. (2)
Test                                                                  | PASS |

就算是將最後一個步驟硬再套上一層 Run Keyword If Not L10N 也沒問題。在 RIDE 下,highlight 也能正常地顯示:

RFKeywordDevRunXXX/ride.png

最後,官方文件提到如果有用到 BuiltIn.run_keyword() 的話,必須要額外做註冊的動作。

The only catch with using methods from BuiltIn is that all run_keyword method variants must be handled specially. Methods that use run_keyword methods have to be registered as run keywords themselves using register_run_keyword method in BuiltIn module. This method’s documentation explains why this needs to be done and obviously also how to do it.

Using BuiltIn library
— Robot Framework User Guide

register_run_keyword() 是這麼寫的:

BuiltIn.py

def register_run_keyword(library, keyword, args_to_process=None):
    """Registers 'run keyword' so that its arguments can be handled correctly.

    1) Why is this method needed

    Keywords running other keywords internally (normally using `Run Keyword`
    or some variants of it in BuiltIn) must have the arguments meant to the
    internally executed keyword handled specially to prevent processing them
    twice. This is done ONLY for keywords registered using this method.

    If the register keyword has same name as any keyword from Robot Framework
    standard libraries, it can be used without getting warnings. Normally
    there is a warning in such cases unless the keyword is used in long
    format (e.g. MyLib.Keyword).

    Starting from Robot Framework 2.5.2, keywords executed by registered run
    keywords can be tested with dryrun runmode with following limitations:
    - Registered keyword must have 'name' argument which takes keyword's name or
    Registered keyword must have '*names' argument which takes keywords' names
    - Keyword name does not contain variables
    ...

for name in [attr for attr in dir(_RunKeyword) if not attr.startswith('_')]:
    register_run_keyword('BuiltIn', getattr(_RunKeyword, name)) 1
1 包括 BuiltIn Library 也會將自己的 run keywords 做註冊的動作。

就這個例子而言,沒有做註冊的動作也沒問題,不過還是照規定來好了:

examples/runkw/L10NLibrary.py

from robot.libraries.BuiltIn import BuiltIn, register_run_keyword

_builtin = BuiltIn()

class L10NLibrary:

  def run_keyword_if_not_l10n(self, name, *args):
    l10n = _builtin.get_variable_value('${L10N}')
    if l10n is None: _builtin.run_keyword(name, *args)

register_run_keyword('L10NLibrary', L10NLibrary.run_keyword_if_not_l10n)