0

Scenario 1

Here is a multiple level inheritance test framework, where each type Foo, Bar, Baz etc is tested in its own class, structured like this:

Foo_Test -> Foo_Lib -> Base

Bar_Test -> Bar_Lib -> Base

Baz_Test -> Baz_Lib -> Base

Scenario 1 shows the code for Foo: the others would be the same. This works fine for tests that exercise only one type Foo OR Bar OR Baz, but Base contains elements that make it a singleton class, so this structure prevents combining, say, Foo AND Bar types in a single test.

Scenario 2

Here Foo_Lib and Bar_Lib have been converted to Free_Foo_Lib and Free_Bar_Lib. These no longer inherit from Base; instead they are each initialised with same unique Base instance. Calls to self.any_base_method(...) are replaced by calls to self.base.any_base_method(...), but otherwise their code is unchanged.

Using Free_Foo_Lib and Free_Bar_Lib I can now write a single test that exercises Foo and Bar types as shown. The down side is that these converted libraries no longer fit the legacy tests in Foo_Test and Bar_Test.

I still want to support these legacy tests, without modifying them. They access functions from their respective libraries and from Base via self, invoking self.any_lib_method(...) or self.any_base_method(...). Since I have broken their supporting inheritance chains, I need to replace them with something that looks (to them) the same.

Scenario 3

Here New_Foo_Test inherits from New_Foo_Lib instead of from Foo_Lib, but the tests within it are unchanged from the old Foo_Test.

New_Foo_Lib has become a wrapper for the methods in Free_Foo_Lib and Base, inheriting from both classes, calling their respective __init__ methods within its own. To do this, New_Foo_Lib.__init__ first calls Base.__init__(self), then 'bootstraps' itself to supply the second, my_base, parameter needed by Free_Foo_Lib.__init__.

Running the sample code confirms that foo_test() in Scenario 3 behaves the same as foo_test() in Scenario 1.

class Base(object):
    def __init__(self):
        print('Only 1 Base instance allowed')

    def base_method(self):
        print("base method")

# Scenario 1: Original multiple level inheritance, for foo tests
class Foo_Lib(Base):

    def lib_method(self):
        print("foo method")

    def lib_calling_base(self):
        print("foo method calls base")
        self.base_method()

class Foo_Test(Foo_Lib):

    def foo_test(self):
        self.base_method()
        self.lib_method()
        self.lib_calling_base()

# Scenario 2: Multi-type foo, bar test using composition
class Free_Foo_Lib(object):
    def __init__(self, my_base):
        self.base = my_base
        print("Free_Foo_Lib has base {}".format(my_base))

    def lib_method(self):
        print("foo method")

    def lib_calling_base(self):
        print("foo method calls base")
        # Calls to base must now be via self.base, not self
        self.base.base_method()

class Free_Bar_Lib(object):
    def __init__(self, my_base):
        self.base = my_base
        print("Free_Bar_Lib has base {}".format(my_base))

    def lib_method(self):
        print("bar method")

class Foo_Bar_Test(object):

    def foo_bar_test(self):
        self.base = Base()
        self.foo_lib = Free_Foo_Lib(self.base)
        self.bar_lib = Free_Bar_Lib(self.base)
        self.base.base_method()
        self.foo_lib.lib_method()
        self.foo_lib.lib_calling_base()
        self.bar_lib.lib_method()

# Scenario 3:  Foo tests still see inheritance, but
# now supported by composition lib 'under the hood'
class New_Foo_Lib(Base, Free_Foo_Lib):
    def __init__(self):
        Base.__init__(self)
        Free_Foo_Lib.__init__(self, self)

class New_Foo_Test(New_Foo_Lib):

    def foo_test(self):
        self.base_method()
        self.lib_method()
        self.lib_calling_base()

print('Scenario 1: Original foo tests')
x = Foo_Test()
x.foo_test()
print('')
print('Scenario 2: Multi-type foo, bar test')
y = Foo_Bar_Test()
y.foo_bar_test()
print('')
print('Scenario 3: New foo tests run as original')
z = New_Foo_Test()
z.foo_test()

Output

Scenario 1: Original foo tests
Only 1 Base instance allowed
base method
foo method
foo method calls base
base method

Scenario 2: Multi-type foo, bar test
Only 1 Base instance allowed
Free_Foo_Lib has base <__main__.Base object at 0x0000000003456E10>
Free_Bar_Lib has base <__main__.Base object at 0x0000000003456E10>
base method
foo method
foo method calls base
base method
bar method

Scenario 3: New foo tests run as original
Only 1 Base instance allowed
Free_Foo_Lib has base <__main__.New_Foo_Test object at 0x0000000003456F60>
base method
foo method
foo method calls base
base method

Summary

This is what I want to be able to do, to 're-plumb' the legacy test cases with flexible libraries which aren't constrained by inheritance from Base. So what is my question? It's about the 'bootstrap' initialiser:

Free_Foo_Lib.__init__(self, self)

  1. I've googled for python __init__(self, self) but not found it. So is it a part of any recognised pattern?

  2. If not, is there a recognised pattern that could be used instead, which (a) keeps the legacy tests unchanged, (b) makes only minimal changes to the libraries, but enables (c) testing Foo and Bar together?

  3. Looking at the output of scenario 3, New_Foo_Lib contains New_Foo_Test as its base. This apparent inversion seems confusing, but would there actually be any undesirable side effects from it? (My experience with it suggests not.)

David Wallace
  • 212
  • 4
  • 12
  • I'm afraid I don't have the mental capacity to follow your 6500 character long question right now, but I'm pretty sure that your design could be improved with some cooperative inheritance. As far as I can tell, your scenario 3 is an ugly and confusing mix of composition and inheritance. If the problem with scenario 1 is that `Base.__init__` is called multiple times, cooperative inheritance can solve that problem. You can find an example of cooperative inheritance in [this answer I wrote](https://stackoverflow.com/a/50465583). (TL;DR: replace `Base.__init__(self)` with `super().__init__()`.) – Aran-Fey May 25 '19 at 13:32
  • @Aran-Fey Thanks for the link. I'd actually already looked briefly at your answer before I posed the question, and now I've looked again more closely to see if I could somehow introduce a mixin to scenario 3, but I don't think it applies. Just to clarify the intent, scenario 1 is legacy and to be left alone as far as possible. Scenario 2 is new development which absolutely depends on composition of the different type libraries because, and I failed to make that clear in the example, Type A, Type B, (and Type C and D etc) libraries will share methods with the same name and signature. – David Wallace May 26 '19 at 20:51
  • Scenario 3 is a way to leverage the libraries now modified to support scenario 2, so that they can also support the legacy tests of scenario 1, without changing those tests. So you're quite correct to describe it a mix of composition and inheritance. I should also make it clear that the tests run in isolation, so legacy tests will never interact with new composition tests. While it's confusing, it's also, at least to me, intriguing. The `New_Type_A_Lib` `__init__()` method bootstraps itself with the first superclass to supply itself as an argument to the second superclass. – David Wallace May 26 '19 at 20:57
  • Question rewritten more concisely in Foo Bar idiom – David Wallace May 30 '19 at 21:52

0 Answers0