Object Oriented Turtle

Data and behavior are combined into one object. Client calls a turtle class instance. Turtle class needs to keep track of the Turtle ‘State’.

The state is defined here for simplicity as (position, angle, pen’s state - up or down).

In this design, the turtle class holds the turtle state and mutates it. This makes it quite simple to wrap the python turtle API and even “extend” it.

Our tootle class has a few methods : - move(distance) - left(angle) - right(angle) - penup() - pendown()

Side note : we use pint for unit of measure (radians / degrees)

Client code

The client code is quite simple. We create a Tootle instance named ‘tt’

../01/client.py
1
2
3
def triangle():
    tt = tootle.Tootle()
    dist = 200

And we call Tootle’s methods to draw a triangle

../01/client.py
1
2
3
4
5
6
7
8
9
def triangle():
    tt = tootle.Tootle()
    dist = 200

    tt.move(dist)
    tt.left(120)
    tt.move(dist)
    tt.left(120)
    tt.move(dist)

Tootle code

The tootle code is usual object oriented code

We use an enum for the two possible penstate values, as well as having penstate as a type.

../01/tootle.py
1
2
3
4
# We use enum for simple values as type
class PenState(enum.Enum):
    UP = -1
    DOWN = 1

We also have a Tootle class, wrapping the existing python Turtle class. It exposes the state as members of this class.

../01/tootle.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# The usual OO inheritance interface
# taking turtle.Turtle as an unknown black box.
# We provide shortcuts to some of its methods and attributes here for interfacing with turtle,
# while trying to keep the turtle API small.
class Tootle(turtle.Turtle):
    """
    Inheriting from provided Turtle class, OO style.
    >>> t = Tootle()


    """
    @property
    def position(self):
        return super().position() * ureg.pixel

    @property
    def angle(self):
        return super().heading() * ureg.degrees

    @property
    def penState(self):
        return super().pen().get('pendown')

    def move(self, distance: int):
        super().forward(distance=distance)

    def right(self, angle: int):
        super().right(angle * ureg.degrees)

    def left(self, angle: int):
        super().left(angle * ureg.degrees)

    def penup(self):
        super().penup()

    def pendown(self):
        super().pendown()

It also provides methods matching our API, that are used to mutate the state (via the python Turtle instance).

../01/tootle.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# The usual OO inheritance interface
# taking turtle.Turtle as an unknown black box.
# We provide shortcuts to some of its methods and attributes here for interfacing with turtle,
# while trying to keep the turtle API small.
class Tootle(turtle.Turtle):
    """
    Inheriting from provided Turtle class, OO style.
    >>> t = Tootle()


    """
    @property
    def position(self):
        return super().position() * ureg.pixel

    @property
    def angle(self):
        return super().heading() * ureg.degrees

    @property
    def penState(self):
        return super().pen().get('pendown')

    def move(self, distance: int):
        super().forward(distance=distance)

    def right(self, angle: int):
        super().right(angle * ureg.degrees)

    def left(self, angle: int):
        super().left(angle * ureg.degrees)

    def penup(self):
        super().penup()

    def pendown(self):
        super().pendown()

Notice also how the code uses docstrings and doctests to certify the documentation stays uptodate.

If you know python, this code should have no mystery for you.

Tootle test

Since we want our code to work, we need to test it.

First we do not want to display a graphical “TurtleScreen” everytime we run a test. This is quite tricky to do, and for now we will rely on a monkey patch. TODO

Then we define our test class, using the unittest core library

../01/test_tootle.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    since the inheritance mechanism uses the logic of the super class and related class hierarchies.
    Also any action with side effect will modify the environment, which we don't control by definition,
    by changing the start position of the turtle and will affect the following test, for example.

    What we can do however, is that our own code is using the rest of the code as expected, using a mock.
    The mock will prevent any side effect by intercepting any procedure call, and register counters,
    before it interferes with the outside world. but we need to investigate the superclass hierarchy to understand
    its behavior and relationship.This breaks the original encapsulation intent...

    Anyway, it is better than not testing, but this tests almost nothing.
    It will not test the whole behavior, and it doesn't scale to test integration of multiple components.
    It is also quite fragile with inheritance, as we have to plug our mock in the exact place, which can be quite hacky
    """

    @mock.patch('turtle.TurtleScreen', autospec=True)
    def setUp(self, mock_tscreen):
        self.t = tootle.Tootle()
        # TODO ... currently mock breaks the turtle...
        assert mock_tscreen.called_with()

    def check_move(self, dist: int):
        """
        Implements one check of move
        :param dist: the distance to move
        :return:
        """
        # get position before
        p0 = self.t.position

        # get position after
        p1 = self.t.position

        assert p1 - p0 == dist

    @mock.patch("turtle.Turtle.forward")
    def test_move(self, mockturtle):
        """ Testing multiple values of distance"""
        for d in [random.randint(0, self.max_test_dist) for _ in range(20)]:

You can notice how we have a few ‘check_’ functions that verify the behavior of a Turtle method for a specific parameter.

../01/test_tootle.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    since the inheritance mechanism uses the logic of the super class and related class hierarchies.
    Also any action with side effect will modify the environment, which we don't control by definition,
    by changing the start position of the turtle and will affect the following test, for example.

    What we can do however, is that our own code is using the rest of the code as expected, using a mock.
    The mock will prevent any side effect by intercepting any procedure call, and register counters,
    before it interferes with the outside world. but we need to investigate the superclass hierarchy to understand
    its behavior and relationship.This breaks the original encapsulation intent...

    Anyway, it is better than not testing, but this tests almost nothing.
    It will not test the whole behavior, and it doesn't scale to test integration of multiple components.
    It is also quite fragile with inheritance, as we have to plug our mock in the exact place, which can be quite hacky
    """

    @mock.patch('turtle.TurtleScreen', autospec=True)
    def setUp(self, mock_tscreen):
        self.t = tootle.Tootle()
        # TODO ... currently mock breaks the turtle...
        assert mock_tscreen.called_with()

    def check_move(self, dist: int):
        """
        Implements one check of move
        :param dist: the distance to move
        :return:
        """
        # get position before
        p0 = self.t.position

        # get position after
        p1 = self.t.position

        assert p1 - p0 == dist

    @mock.patch("turtle.Turtle.forward")
    def test_move(self, mockturtle):
        """ Testing multiple values of distance"""
        for d in [random.randint(0, self.max_test_dist) for _ in range(20)]:

Then we have a few ‘test_method’ that actually run these checks for a larger number of parameters. It helps us have a better coverage of the data space.

../01/test_tootle.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    since the inheritance mechanism uses the logic of the super class and related class hierarchies.
    Also any action with side effect will modify the environment, which we don't control by definition,
    by changing the start position of the turtle and will affect the following test, for example.

    What we can do however, is that our own code is using the rest of the code as expected, using a mock.
    The mock will prevent any side effect by intercepting any procedure call, and register counters,
    before it interferes with the outside world. but we need to investigate the superclass hierarchy to understand
    its behavior and relationship.This breaks the original encapsulation intent...

    Anyway, it is better than not testing, but this tests almost nothing.
    It will not test the whole behavior, and it doesn't scale to test integration of multiple components.
    It is also quite fragile with inheritance, as we have to plug our mock in the exact place, which can be quite hacky
    """

    @mock.patch('turtle.TurtleScreen', autospec=True)
    def setUp(self, mock_tscreen):
        self.t = tootle.Tootle()
        # TODO ... currently mock breaks the turtle...
        assert mock_tscreen.called_with()

    def check_move(self, dist: int):
        """
        Implements one check of move
        :param dist: the distance to move
        :return:
        """
        # get position before
        p0 = self.t.position

        # get position after
        p1 = self.t.position

        assert p1 - p0 == dist

    @mock.patch("turtle.Turtle.forward")
    def test_move(self, mockturtle):
        """ Testing multiple values of distance"""
        for d in [random.randint(0, self.max_test_dist) for _ in range(20)]:

Despite these efforts to test our turtle, there are many cases that we did not consider. For example : - We test only from the initial turtle state, but what if the method behavior depends on the turtle state ? - We test only a very small set of possible values, what is the method behavior is different for some untested value ? - How will be the behavior for “unexpected” inputs ? Will it crash ? throw and exception ? which one ? We do not test the unexpected (obviously)

These are the current limitation of basic python tests. We will attempt to improve our test coverage in the following chapters.

Conclusion

Pros:

  • Familiar for most python programmers.

Cons :

  • Stateful. Hard to test methods independently from the state.
  • Inheritance effectively provides a blackbox, that can be hard to setup for tests.
  • Cannot compose between method calls, we have to actually do one behavior at a time.
  • Hard coded dependencies in module, the client needs to bring in everything.