Skip to content

Commit

Permalink
add exercise for facade and testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Kristian Rother committed Jan 16, 2024
1 parent c8c4f51 commit aa73bf2
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 14 deletions.
9 changes: 8 additions & 1 deletion index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ Setting up a Python Project
getting_started/git_repo.rst
getting_started/structure.rst

Writing Automated Tests
-----------------------

.. toctree::
:maxdepth: 1

testing/facade.rst
testing/unit_test.rst

Functions
---------
Expand All @@ -45,7 +53,6 @@ Functions
functions/levels.rst
functions/function_parameters.rst
functions/scope.rst
functions/lambda_functions.rst
functions/generators.rst
functions/functools.rst
functions/decorators.rst
Expand Down
24 changes: 11 additions & 13 deletions software_engineering/prototype_opencv.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,30 @@
TILE_SIZE = 64


def draw_player(background, player, x, y):
"""draws the player image on the screen"""
frame = background.copy()
xpos, ypos = x * TILE_SIZE, y * TILE_SIZE
frame[ypos : ypos + TILE_SIZE, xpos : xpos + TILE_SIZE] = player
cv2.imshow("frame", frame)


def double_size(img):
def read_image(filename):
"""returns an image twice as big"""
img = cv2.imread(filename)
return np.kron(img, np.ones((2, 2, 1), dtype=img.dtype))


# load image
player = double_size(cv2.imread("tiles/deep_elf_high_priest.png"))
player_img = read_image("tiles/deep_elf_high_priest.png")

def draw(x, y):
"""draws the player image on the screen"""
frame = np.zeros((SCREEN_SIZE_Y, SCREEN_SIZE_X, 3), np.uint8)
xpos, ypos = x * TILE_SIZE, y * TILE_SIZE
frame[ypos : ypos + TILE_SIZE, xpos : xpos + TILE_SIZE] = player_img
cv2.imshow("frame", frame)

# create black background image with BGR color channels
background = np.zeros((SCREEN_SIZE_Y, SCREEN_SIZE_X, 3), np.uint8)

# starting position of the player in dungeon
x, y = 4, 4

exit_pressed = False

while not exit_pressed:
draw_player(background, player, x, y)
draw(x, y)

# handle keyboard input
key = chr(cv2.waitKey(1) & 0xFF)
Expand Down
114 changes: 114 additions & 0 deletions testing/facade.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
The Facade Pattern
==================

Before you can write automated tests, you need to make sure that your code is testable.
It is not a great idea to test any function or class in your program,
because that makes the program harder to modify in the future.
Whatever you test, you want to be stable and not change very often.
Testable code means that you need to define an **interface** you are testing against.

Also, some things are harder to test than others, graphics and keyboard input for instance.
We won't test them for now. Instead, we want to make the code more testable
by **separating the graphics engine and game logic**.

The Design
----------

In the `Facade Pattern <https://sourcemaking.com/design_patterns/facade>`__, you define a single class
that serves as the interface to an entire subsysten.
We will define such a Facade class for the game logic named ``DungeonExplorer``:

.. code:: python3
class DungeonExplorer(BaseModel):
player: Player
level: Level
def get_objects() -> list[DungeonObject]:
"""Returns everything in the dungeon to be used by a graphics engine"
...
def execute_command(cmd: str) -> None:
"""Performs a player action, such as 'left', 'right', 'jump', 'fireball'"""
...
Note that the attributes ``player`` and ``level`` of the game might change in the future.
We will treat them as private.
All the communication should happen through the two methods.

In the following exercise, you refactor the code to use the Facade pattern.

Step 1: Separate Modules
------------------------

Split the existing code into two Python modules ``graphics_engine.py`` and ``game_logic.py``.
For each paragraph of code decide, which of the two modules it belongs to.

Step 2: Implement the Facade class
----------------------------------

Copy the skeleton code for the ``DungeonExplorer`` class to ``game_logic.py``.
Leave the methods empty for now.

Step 3: Define a class for data exchange
----------------------------------------

In the ``get_objects()`` method, we use the type ``DungeonObject`` to send everything that
should be drawn to the graphics engine.
This includes walls, the player for now, but will include more stuff later.
Define it as follows:

.. code:: python3
class DungeonObject(BaseModel):
position: Position
name: str
Example objects could be:

.. code:: python3
DungeonObject(Position(x=1, y=1), "wall")
DungeonObject(Position(x=4, y=4), "player")
.. note::

This is really a very straightforward approach to send the information for drawing.
In fact, it makes a couple of things very hard, e.g. animation.
This is an example of a design decision: we choose that we do not want animations in the game.
Our design makes adding them more expensive.

Step 4: Implement the get_objects method
----------------------------------------

Implement the ``get_objects()`` method from scratch.
Create a list of the player and all walls as a list of ``DungeonObject``.

Step 5: Implement the execute_command method
--------------------------------------------

Move the code you have for handling keyboard input into the ``execute_command()`` method.
Replace the keys by explicit commands like `"left"`, `"right"` etc.
The idea behind that is that we do not want the game logic to know anything about
which key you press to walk right. This belongs to the user interface.

Step 6: Import the Facade class
-------------------------------

In the module ``graphics_engine.py``, import the Facade class ``DungeonExplorer``.
The only things the user interface needs to know about are the Facade class and
the data exchange class ``DungeonObject`` (although we do not have to import the latter).

Create an instance of it.

Step 7: Adjust the graphics engine
----------------------------------

Make sure the graphics engine does the following:

- it calls ``DungeonExplorer.get_objects`` in the draw function.
- it does not access the level or player attributes in the draw function.
- it translates the keys to commands
- it calls the ``DungeonExplorer.execute_command`` method.
46 changes: 46 additions & 0 deletions testing/unit_test.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Unit Tests
==========

In this short exercise, we will write a test against the Facade.

Step 1: Install pytest
----------------------

Make sure pytest is installed:

::

pip install pytest

Step 2: Create a test
---------------------

Create a file ``test_game_logic.py``. In it, you need the folowing code:

.. code:: python3
from game_logic import DungeonExplorer, DungeonObject
def test_move():
dungeon = DungeonExplorer(
player=Player(Position(x=1, y=1),
... # add other attributes if necessary
dungeon.execute_command("right")
assert DungeonObject(Position(x=2, y=1), "player") in dungeon.get_objects()
A typical automated test consists of three parts:

1. setting up test data (fixtures)
2. executing the code that is tested
3. checking the results against expected values

Step 3: Run the test
--------------------

Run the tests from the terminal with:

::

pytest

You should see a message that the test either passes or fails.

0 comments on commit aa73bf2

Please sign in to comment.