Robot FrameworkのキーワードEvaluateでPythonのコードを実行する

この記事は 「Robot Framework Advent Calendar 2017 - Qiita」 の17日目の記事です。

Robot Frameworkの組込キーワード Evaluate を使うとPythonコードを実行できます。

 
今回は Evaluate キーワードを試してみます。

 
目次

 

環境

 

文字列のフォーマット

Robot Framework単独では難しいのが文字列のフォーマットです。

一方、Pythonで書くと楽です。

今回は

  • %演算子
  • str.format()関数
  • f-strings
    • Python3.6以上

を試します。

Robotframework-DebugLibraryの rfdebug コマンドでREPLを起動し、それぞれを確認してみます。

$ rfdebug
====================
Robot Debug9 R36Le9
====================
RFDEBUG REPL
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.
> ${結果} =  Evaluate  '%06d %s' % (1, 'ゼロ埋めです')
# ${結果} = '000001 ゼロ埋めです'
> ${結果} =  Evaluate  'foo {}'.format('bar')
# ${結果} = 'foo bar'
> ${結果2} =  Evaluate  'foo {}'.format('bar')
# ${結果2} = 'foo bar'
> ${結果3} =  Evaluate  f'baz ${結果2}'
# ${結果3} = 'baz foo bar'

 
いずれも動作しました。

 

Pythonクラスのインスタンスを取得する

Evaluateのソースコードを読むと、内部ではPythoneval() 関数を使っています。
https://github.com/robotframework/robotframework/blob/3.0.2/src/robot/libraries/BuiltIn.py#L2963

また、Evaluateの引数のmoduleやnamespaceは __import__() 関数でimportされています。
https://github.com/robotframework/robotframework/blob/3.0.2/src/robot/libraries/BuiltIn.py#L2997

そのため、Pythonクラスのインスタンスを取得できそうです。

 

Robot FrameworkのLibraryで対象モジュールをimport済の場合
sys.modulesを使う

例えば、

*** Settings ***
Library  SeleniumLibrary
Library  DebugLibrary

*** TestCases ***
読み込み済モジュールに含まれるクラスをインスタンス化する
    Debug

とした時に、

  • SeleniumLibraryをimport済
  • Headless Firefoxの設定を行うために selenium.webdriver.firefox.options.Option クラスのインスタンスを取得したい

とします。

 
この場合、 sys.modulesselenium パッケージが追加されています。

 
そのため、

${options} =  Evaluate  sys.modules['selenium.webdriver.firefox.options'].Options()  sys

としてOptionsクラスのインスタンスを取得できます。

なお、引数は

  • 第1引数
    • sys.modulesselenium.webdriver.firefox.options にある Options クラスをインスタンス化する処理
  • 第2引数
    • 第1引数で使われているモジュールのうち、追加でimportが必要なモジュール名
      • 今回は sys モジュールを使用

です。

 
本当に取得できたのか、REPLで確認してみます。

# 上記例が含まれるRobot Frameworkのテストケースを実行
$ robot test_get_instance_from_already_imported_module.robot 
...

# キーワードDebugに到達したため、REPLが起動
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.

# Robot Frameworkライブラリの確認:SeleniumLibraryあり
> libs
< Imported libraries:
   BuiltIn 3.0.2
       An always available standard library with often needed keywords.
   DebugLibrary 1.0.2
       Debug Library for RobotFramework
   Easter 
   Reserved 
   SeleniumLibrary 3.0.1
       SeleniumLibrary is a web testing library for Robot Framework.
< Bultin libraries:
   BuiltIn 
   Collections 
   DateTime 
   Dialogs 
   Easter 
   OperatingSystem 
   Process 
   Remote 
   Reserved 
   Screenshot 
   String 
   Telnet 
   XML 

# sys.modulesにあるか確認:あった
> Evaluate  sys.modules['selenium.webdriver.firefox.options']  sys
< <module 'selenium.webdriver.firefox.options' from '/path/to/rfenv363/lib/python3.6/site-packages/selenium/webdriver/firefox/options.py'>

# 上記のコードを試す:インスタンスを取得できた
> ${options} =  Evaluate  sys.modules['selenium.webdriver.firefox.options'].Options()  sys
# ${options} = <selenium.webdriver.firefox.options.Options object at 0x10eee60b8>

   

Robot Frameworkを対象モジュールをimportしていない場合

例えば、

*** Settings ***
Library  DebugLibrary

*** TestCases ***
読み込まれていないモジュールに含まれるクラスをインスタンス化する
    Debug

とした時に、

とします。

 

sys.modulesを使う場合

sys.modulesを使う場合は、Libraryでimportした時と同じです。

# 上記のテストケースを実行
$ robot test_get_instance_from_no_import_module.robot 
...

# キーワードDebugに到達したため、REPLが起動
>>>>> Enter interactive shell
Only accepted plain text format keyword seperated with two or more spaces.
Type "help" for more information.

# sysとdistutils.versionをimportして、LooseVersionクラスのインスタンスを取得
> Evaluate  sys.modules['distutils.version'].LooseVersion('3.0.2')  sys,distutils.version
< LooseVersion ('3.0.2')

 

__import__()関数を使う場合

Pythonでのimport同様、 __import__() 関数も使えます。
__import__ - 2. 組み込み関数 — Python 3.6.3 ドキュメント

ただ、

name 変数が package.module 形式であるとき、通常は、name で指名されたモジュール ではなく、最上位のパッケージ (最初のドットまでの名前) が返されます。しかしながら、空でない fromlist 引数が与えられると、 name で指名されたモジュールが返されます。

__import__ - 2. 組み込み関数 — Python 3.6.3 ドキュメント

のため、第1引数だけ指定すると

$ robot test_get_instance_from_no_import_module.robot 
...
> Evaluate  __import__('distutils.version').LooseVersion('3.0.2')
! keyword: Evaluate  __import__('distutils.version').LooseVersion('3.0.2')
! Evaluating expression '__import__('distutils.version').LooseVersion('3.0.2')' failed: AttributeError: module 'distutils' has no attribute 'LooseVersion'

と、エラーになります。

そのため、 fromlist 引数も使用します。

$ robot test_get_instance_from_no_import_module.robot 
...
> Evaluate  __import__('distutils.version', fromlist=['LooseVersion']).LooseVersion('3.0.2')
< LooseVersion ('3.0.2')

 

importlib.import_module()を使う場合

__import__() 関数の説明の中で

__import__() を直接使用することも推奨されず、 importlib.import_module() の方が好まれます。

__import__ - 2. 組み込み関数 — Python 3.6.3 ドキュメント

とあったため、 importlib.import_module() を見てみます。

 
importlib.import_module()の説明には

import_module() 関数は importlib.__import__() を単純化するラッパーとして働きます。つまり、この関数のすべての意味は importlib.__import__() から受け継いでいます。これらの2つの関数の最も重要な違いは、 import_module() が指定されたパッケージやモジュール (例えば pkg.mod) を返すのに対し、 __import__() はトップレベルのパッケージやモジュール (例えば pkg) を返すことです。

importlib.import_module(name, package=None) - 31.5. importlib — import の実装 — Python 3.6.3 ドキュメント

とありました。

試してみます。

$ robot test_get_instance_from_no_import_module.robot 
...
> Evaluate  importlib.import_module('distutils.version').LooseVersion('3.0.2')  importlib
< LooseVersion ('3.0.2')

同じ結果になりました。

個人的には __import__() よりも importlib.import_module() の方が分かりやすいです。

 

ソースコード

GitHubに上げました。builtin_library_samples/evaluate ディレクリの中が今回のものです。
thinkAmi-sandbox/RobotFramework-sample: Robot Framewrok samples