0
# Library Development
1
2
Robot Framework provides comprehensive APIs for creating custom test libraries. Libraries can implement keywords using static, dynamic, or hybrid approaches, with full support for type conversion, argument handling, and test execution integration.
3
4
## Capabilities
5
6
### Library Decorators
7
8
Decorators for controlling keyword discovery and library behavior.
9
10
```python { .api }
11
@keyword(name=None, tags=(), types=()):
12
"""
13
Decorator to set custom name, tags, and argument types for keywords.
14
15
Args:
16
name: Custom keyword name (defaults to function name)
17
tags: Sequence of tags to assign to the keyword
18
types: Type hints for argument conversion
19
"""
20
21
@library(scope=None, version=None, converters=None, doc_format=None,
22
listener=None, auto_keywords=False):
23
"""
24
Decorator to control library-wide settings and keyword discovery.
25
26
Args:
27
scope: Library scope ("GLOBAL", "SUITE", "TEST", "TASK")
28
version: Library version
29
converters: Custom type converters dictionary
30
doc_format: Documentation format ("ROBOT", "HTML", "TEXT", "REST")
31
listener: Listener instance for library events
32
auto_keywords: Whether to automatically discover keywords
33
"""
34
35
@not_keyword:
36
"""
37
Decorator to prevent functions from being exposed as keywords.
38
"""
39
```
40
41
**Usage Examples:**
42
43
```python
44
from robot.api.deco import keyword, library, not_keyword
45
46
@library(scope='SUITE', version='1.0')
47
class MyLibrary:
48
49
@keyword('Custom Keyword Name', tags=['web', 'ui'])
50
def original_function_name(self, text, timeout=5):
51
"""Keyword with custom name and tags."""
52
pass
53
54
@keyword(types=[int, float])
55
def calculate_sum(self, num1, num2):
56
"""Keyword with type conversion."""
57
return num1 + num2
58
59
@not_keyword
60
def helper_method(self):
61
"""This method won't be exposed as a keyword."""
62
pass
63
64
def regular_keyword(self, arg1, arg2='default'):
65
"""Regular keyword discovered automatically."""
66
pass
67
```
68
69
### Exception Classes
70
71
Specialized exceptions for controlling test execution flow and reporting.
72
73
```python { .api }
74
class Failure(AssertionError):
75
"""
76
Report failed validation with HTML support.
77
78
Args:
79
message: Exception message
80
html: When True, message is treated as HTML
81
"""
82
def __init__(self, message: str, html: bool = False): ...
83
84
class ContinuableFailure(Failure):
85
"""
86
Report failed validation but allow test execution to continue.
87
"""
88
89
class Error(RuntimeError):
90
"""
91
Report error in execution (incorrect keyword usage).
92
93
Args:
94
message: Exception message
95
html: When True, message is treated as HTML
96
"""
97
def __init__(self, message: str, html: bool = False): ...
98
99
class FatalError(Error):
100
"""
101
Report error that stops the whole test execution.
102
"""
103
104
class SkipExecution(Exception):
105
"""
106
Mark the executed test or task as skipped.
107
108
Args:
109
message: Skip reason message
110
html: When True, message is treated as HTML
111
"""
112
def __init__(self, message: str, html: bool = False): ...
113
```
114
115
**Usage Examples:**
116
117
```python
118
from robot.api import Failure, ContinuableFailure, Error, SkipExecution
119
120
class ValidationLibrary:
121
122
def validate_user_input(self, input_data):
123
if not input_data:
124
raise Failure("Input data cannot be empty")
125
126
if len(input_data) < 3:
127
raise ContinuableFailure(
128
"Input too short, continuing with <em>warning</em>",
129
html=True
130
)
131
132
def connect_to_service(self, url):
133
if not url.startswith(('http://', 'https://')):
134
raise Error("Invalid URL format provided")
135
136
def run_critical_test(self):
137
try:
138
self.perform_critical_operation()
139
except CriticalSystemError:
140
raise FatalError("Critical system failure - stopping all tests")
141
142
def check_environment(self):
143
if not self.is_test_environment_ready():
144
raise SkipExecution("Test environment not ready, skipping test")
145
```
146
147
### Logging API
148
149
Programmatic logging interface for test libraries.
150
151
```python { .api }
152
def write(msg: str, level: str = "INFO", html: bool = False):
153
"""Write message to log file using specified level."""
154
155
def trace(msg: str, html: bool = False):
156
"""Write TRACE level message (not logged by default)."""
157
158
def debug(msg: str, html: bool = False):
159
"""Write DEBUG level message (not logged by default)."""
160
161
def info(msg: str, html: bool = False, also_console: bool = False):
162
"""Write INFO level message."""
163
164
def warn(msg: str, html: bool = False):
165
"""Write WARN level message (also shown in console)."""
166
167
def error(msg: str, html: bool = False):
168
"""Write ERROR level message (also shown in console)."""
169
170
def console(msg: str, newline: bool = True, stream: str = "stdout"):
171
"""Write message directly to console."""
172
```
173
174
**Usage Examples:**
175
176
```python
177
from robot.api import logger
178
179
class DatabaseLibrary:
180
181
def connect_to_database(self, connection_string):
182
logger.info(f"Connecting to database: {connection_string}")
183
184
try:
185
self.connection = self._establish_connection(connection_string)
186
logger.debug("Database connection established successfully")
187
except Exception as e:
188
logger.error(f"Failed to connect to database: {e}")
189
raise
190
191
def execute_query(self, query):
192
logger.trace(f"Executing SQL query: {query}")
193
194
if "DROP" in query.upper():
195
logger.warn("Executing potentially destructive query", html=False)
196
197
result = self.connection.execute(query)
198
logger.info(f"Query returned {len(result)} rows")
199
200
# Log HTML table of results
201
html_table = self._format_as_html_table(result)
202
logger.info(html_table, html=True)
203
204
return result
205
206
def backup_database(self):
207
logger.console("Starting database backup...", stream="stdout")
208
# Backup logic here
209
logger.console("Database backup completed!", stream="stdout")
210
```
211
212
### Library Interface Base Classes
213
214
Optional base classes for implementing different library APIs.
215
216
```python { .api }
217
class DynamicLibrary:
218
"""
219
Base class for libraries using the dynamic library API.
220
Keywords are discovered and executed dynamically.
221
"""
222
223
def get_keyword_names(self) -> Sequence[str]:
224
"""Return names of keywords this library implements (required)."""
225
226
def run_keyword(self, name: str, args: tuple, named: dict):
227
"""Execute the specified keyword with given arguments (required)."""
228
229
def get_keyword_documentation(self, name: str) -> str:
230
"""Return keyword documentation (optional)."""
231
232
def get_keyword_arguments(self, name: str) -> Sequence:
233
"""Return keyword argument specification (optional)."""
234
235
def get_keyword_types(self, name: str):
236
"""Return keyword argument types (optional)."""
237
238
def get_keyword_tags(self, name: str) -> Sequence[str]:
239
"""Return keyword tags (optional)."""
240
241
def get_keyword_source(self, name: str) -> str:
242
"""Return keyword source information (optional)."""
243
244
class HybridLibrary:
245
"""
246
Base class for libraries using the hybrid library API.
247
Combines static and dynamic approaches.
248
"""
249
250
def get_keyword_names(self) -> Sequence[str]:
251
"""Return names of keywords this library implements (required)."""
252
```
253
254
**Usage Examples:**
255
256
```python
257
from robot.api.interfaces import DynamicLibrary
258
259
class RestApiLibrary(DynamicLibrary):
260
261
def __init__(self, base_url=""):
262
self.base_url = base_url
263
self.session = self._create_session()
264
265
def get_keyword_names(self):
266
return ['GET Request', 'POST Request', 'PUT Request', 'DELETE Request',
267
'Set Base URL', 'Set Headers', 'Verify Status Code']
268
269
def run_keyword(self, name, args, named):
270
method_name = name.lower().replace(' ', '_')
271
method = getattr(self, method_name)
272
return method(*args, **named)
273
274
def get_keyword_documentation(self, name):
275
docs = {
276
'GET Request': 'Send HTTP GET request to specified endpoint',
277
'POST Request': 'Send HTTP POST request with data',
278
'Verify Status Code': 'Verify the HTTP response status code'
279
}
280
return docs.get(name, '')
281
282
def get_keyword_arguments(self, name):
283
args = {
284
'GET Request': ['endpoint', 'params={}', 'headers={}'],
285
'POST Request': ['endpoint', 'data', 'headers={}'],
286
'Verify Status Code': ['expected_code']
287
}
288
return args.get(name, [])
289
290
def get_keyword_tags(self, name):
291
if 'Request' in name:
292
return ['http', 'api']
293
return ['verification']
294
295
# Keyword implementations
296
def get_request(self, endpoint, params=None, headers=None):
297
url = f"{self.base_url}{endpoint}"
298
response = self.session.get(url, params=params, headers=headers)
299
self.last_response = response
300
return response
301
302
def verify_status_code(self, expected_code):
303
actual_code = self.last_response.status_code
304
if actual_code != int(expected_code):
305
raise Failure(f"Expected status {expected_code}, got {actual_code}")
306
```
307
308
### Listener Interfaces
309
310
Monitor and react to test execution events.
311
312
```python { .api }
313
class ListenerV2:
314
"""
315
Base class for listener API version 2.
316
Provides hooks for test execution events.
317
"""
318
319
def start_suite(self, name, attributes): ...
320
def end_suite(self, name, attributes): ...
321
def start_test(self, name, attributes): ...
322
def end_test(self, name, attributes): ...
323
def start_keyword(self, name, attributes): ...
324
def end_keyword(self, name, attributes): ...
325
def log_message(self, message): ...
326
def message(self, message): ...
327
def library_import(self, name, attributes): ...
328
def resource_import(self, name, attributes): ...
329
def variables_import(self, name, attributes): ...
330
def output_file(self, path): ...
331
def close(self): ...
332
333
class ListenerV3:
334
"""
335
Base class for listener API version 3.
336
Enhanced version with additional event information.
337
"""
338
339
# Similar methods to ListenerV2 but with enhanced attributes
340
def start_suite(self, data, result): ...
341
def end_suite(self, data, result): ...
342
def start_test(self, data, result): ...
343
def end_test(self, data, result): ...
344
# Additional methods for control structures
345
def start_if(self, data, result): ...
346
def end_if(self, data, result): ...
347
def start_for(self, data, result): ...
348
def end_for(self, data, result): ...
349
```
350
351
**Usage Examples:**
352
353
```python
354
from robot.api.interfaces import ListenerV2
355
import time
356
357
class TestTimingListener(ListenerV2):
358
359
def __init__(self):
360
self.suite_times = {}
361
self.test_times = {}
362
363
def start_suite(self, name, attributes):
364
self.suite_times[name] = {'start': time.time()}
365
print(f"Suite '{name}' started")
366
367
def end_suite(self, name, attributes):
368
start_time = self.suite_times[name]['start']
369
duration = time.time() - start_time
370
print(f"Suite '{name}' completed in {duration:.2f} seconds")
371
372
def start_test(self, name, attributes):
373
self.test_times[name] = {'start': time.time()}
374
375
def end_test(self, name, attributes):
376
start_time = self.test_times[name]['start']
377
duration = time.time() - start_time
378
status = attributes['status']
379
print(f"Test '{name}' {status} in {duration:.2f} seconds")
380
381
def log_message(self, message):
382
level = message['level']
383
if level == 'ERROR':
384
print(f"ERROR LOGGED: {message['message']}")
385
```
386
387
### Custom Parser Interface
388
389
Create custom parsers for non-standard test data formats.
390
391
```python { .api }
392
class Parser:
393
"""
394
Base class for custom parsers.
395
"""
396
397
extension: Union[str, Sequence[str]] # File extensions this parser handles
398
399
def parse(self, source: Path, defaults) -> TestSuite:
400
"""Parse source file into TestSuite (required)."""
401
402
def parse_init(self, source: Path, defaults) -> TestSuite:
403
"""Parse suite initialization file (optional)."""
404
```
405
406
**Usage Examples:**
407
408
```python
409
from robot.api.interfaces import Parser
410
from robot.api import TestSuite
411
import json
412
from pathlib import Path
413
414
class JsonTestParser(Parser):
415
extension = '.json'
416
417
def parse(self, source: Path, defaults):
418
with open(source) as f:
419
data = json.load(f)
420
421
suite = TestSuite(
422
name=data.get('name', source.stem),
423
doc=data.get('documentation', ''),
424
source=str(source)
425
)
426
427
# Parse test cases from JSON
428
for test_data in data.get('tests', []):
429
test = suite.tests.create(
430
name=test_data['name'],
431
doc=test_data.get('doc', '')
432
)
433
434
# Add keywords from JSON structure
435
for keyword_data in test_data.get('keywords', []):
436
test.keywords.create(
437
name=keyword_data['name'],
438
args=keyword_data.get('args', [])
439
)
440
441
return suite
442
```
443
444
## Types
445
446
```python { .api }
447
# Library scope options
448
Scope = Literal["GLOBAL", "SUITE", "TEST", "TASK"]
449
450
# Documentation format options
451
DocFormat = Literal["ROBOT", "HTML", "TEXT", "REST"]
452
453
# Logging levels
454
LOGLEVEL = Literal["TRACE", "DEBUG", "INFO", "CONSOLE", "HTML", "WARN", "ERROR"]
455
456
# Library interface types
457
Name = str
458
Arguments = Sequence[Union[str, Tuple[str], Tuple[str, Any]]]
459
Documentation = str
460
Tags = Sequence[str]
461
Source = str
462
463
# Parser interface types
464
TestDefaults = object # Default values and settings for test execution
465
466
# Type hint specifications
467
TypeHint = Union[type, str, Tuple["TypeHint", ...]]
468
TypeHints = Union[Mapping[str, TypeHint], Sequence[TypeHint]]
469
470
# Converter function signature
471
Converter = Union[Callable[[Any], Any], Callable[[Any, Any], Any]]
472
```