Tree-based organization of keywords within a Robot Framework test library?

I have an existing Python I/O library that communicates with a piece of test equipment. I have been tasked with modifying it to add Robot Framework hooks such as keyword decorators, listeners, etc.

The Python library implements ~800 commands, each as a Python method. They’re organized into a few child classes, based on the command group. For example, in Python, I might use it like this:

import FooLibrary
foo = FooLibrary()
foo.group1.reset()
foo.group1.start()
foo.group1.doSomething()
foo.group1.stop()
foo.group2.reset()
foo.group2.start()
foo.group2.subgroup1.bar()
foo.group2.subgroup1.baz()
foo.group2.stop()

Is it even possible to keep the tree-based child class structure like this in a Robot Framework library? I’ve read the RF user guide, and didn’t see anything, so I assume the answer is “no”. But either way, I need to make this library usable within Robot Framework. So is there any advice for naming and organizing keywords for a library this big?

I can’t name the keywords something simple like “Reset”, “Start”, and “Stop”, since there are multiple command groups with those names.

And I can’t name them “Reset Group1”, “Reset Group2” etc because the API docs sort alphabetically, that makes it a huge mess since all the groups are out-of-order.

And I can’t make each group a separate Robot Framework library since they share an I/O communication session to the test equipment.

The only solution I can come up with is to have a giant monolithic test library with all 800+ keywords shoved into it, and prefix them with the group name, e.g.: “Group 1 Reset” “Group 2 Reset” and so on.

This doesn’t seem very elegant to me. Is there a better way to structure and name the keywords in my test library?

“Is it even possible to keep the tree-based child class structure like this in a Robot Framework library?”
Yes. The basic idea would be to have an Abstract Base Class (ABC) or protocol which defines the structure a group should have - i.e. that any class should expose & implement/inherit start, stop, reset etc. methods.

Group1 and Group2 would be classes setup to implement/inherit this specification.

FooLibrary would then setup group1 and group2 variables for the types (classes) Group1 and Group2 & expose them. There is a name for this ease-of-use, moving up & exposing of a shortcutting class from the main class in Martin Fowler’s book, Refactoring but the name escapes me now.

A collection could probably serve here. If all the groups have the same definition (start, stop, reset etc.) then keep the ABC/protocol/duck typing setup described above, but instead of setting up a variable for group1, group2 etc. in FooLibrary, instead build a collection of Group types. This will shortern the code & make it far more resiliant to expansion + maintenance changes later on.

Also you could go back to the drawing board here and look into both the (i) iterator & composite design patterns together and (ii) command design pattern which might likely better fit your needs. :slight_smile:

the groups don’t have identical method names unfortunately. A few overlap, but most don’t.

1 Like

I can’t rewrite the library. I am only trying to annotate it with Robot Framework keywords.

1 Like

The library is already setup to expose group1 and group2 and it all works fine in Python. My question is whether or not I can nest keywords as shown in the code example below.

FooLibrary.py:

from robot.api.deco import library, keyword
from robot.api import logger

__all__ = ['FooLibrary']


@library(scope='TEST')
class FooLibrary:

    def __init__(self):
        logger.info(f'FooLibrary constructor id={id(self)}')
        self._group1 = Group1()
        self._group2 = Group2()

    @keyword("Initialize")
    def initialize(self):
        logger.info(f'FooLibrary Initialize id={id(self)}')

    @keyword("Close")
    def close(self):
        logger.info(f'FooLibrary Close id={id(self)}')

    @property
    @keyword("Group1")
    def group1(self):
        return self._group1

    @property
    @keyword("Group2")
    def group2(self):
        return self._group1


class Group1:

    def __init__(self):
        logger.info(f'Group1 constructor id={id(self)}')

    @keyword("Start")
    def start(self):
        logger.info(f'Group1 Start id={id(self)}')

    @keyword("Stop")
    def stop(self):
        logger.info(f'Group1 Stop id={id(self)}')

    @keyword("Do Something")
    def doSomething(self):
        logger.info(f'Group1 Do Something id={id(self)}')


class Group2:

    def __init__(self):
        logger.info(f'Group2 constructor id={id(self)}')

    @keyword("Start")
    def start(self):
        logger.info(f'Group2 Start id={id(self)}')

    @keyword("Stop")
    def stop(self):
        logger.info(f'Group2 Stop id={id(self)}')

test.robot:

*** Settings ***
Library               FooLibrary


*** Test Cases ***

FooTest
    Initialize
    Group1  # [FAIL INSIDE[ No keyword with name 'Group1' found.
    Group1.Start  # [FAIL INSIDE[ No keyword with name 'Group1' found.
    Close

ok working example1, this with properties not keywords:
*.py code

    @library(scope='TEST')
    class FooLibrary:
    ......
        @property
        def group1a(self):
            logger.info(f'group1a method running')
            return self._group1

    class Group1:
    ......
        @property
        def starta(self):
            logger.info(f'Group1 Start id={id(self)}')

*.robot code

${FooLibrary}    Get Library Instance    FooLibrary
${group1a}=    Set Variable    ${FooLibrary.group1a}
${group1a_start}=    Set Variable    ${FooLibrary.group1a.starta}

working example2, this with keywords:

*.py code

@library(scope='TEST')
class FooLibrary:
......
@keyword("group1b")
def group1b(self):
    logger.info(f'group1b method running')
    return self._group1

class Group1:
......
@keyword("startb")    
def startb(self):
    logger.info(f'Group1 Start id={id(self)}')

*.robot code

Library    OperatingSystem
......
*** Test Cases ***
FooTest
    ${group1b}=    Run Keyword    group1b
    BuiltIn.Call Method    ${group1b}    startb