One thing you usually want to do is add unit tests. Otherwise you have to basically constantly run your program and manually verify a bunch of scenarios. Having unit tests lets you saves time in the long run. The other benefit is that if something is really hard to unit test, then it could be indicative of a class that is too complex which helps drive better design.
This is just a rule of thumb though. Sometimes especially if you’re working on something new, you’re not really sure how you’re going to be organizing everything. Often times, I’ll get something basic working and then add unit tests later.
In this case, I feel pretty good about how I want the CLI portion to work so I’ll go ahead and write a test for this.
Verify Tests Run
The first thing I do is just set up a dummy test that fails. I’ve actually worked on a project where I added a test before and turned out it wasn’t actually executing.
import unittest
class TestGameCli(unittest.TestCase):
def test_something(self):
self.assertTrue(False)
Then figure out how to run the test. This is also good info to include in the README.
$ python3 -m unittest test/test*
F
======================================================================
FAIL: test_something (test.test_game_cli.TestGameCli)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/yujincho/code/BattleShipPython/test/test_game_cli.py", line 6, in test_something
self.assertTrue(False)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
Extract parsing function
Right now the GameCli class doesn’t have much to test. It takes in user inputs and just logs some lines. Later we’ll want to verify it actually takes some actions but for now, the main thing our class does is handle user inputs and turns them into something meaningful. So let’s break this out into a standalone function so we can test its behavior.
I’ll move the parse_user_input method into a standalone function.
def parse_user_input(raw_user_input):
# user term to display the game board
show = 'show'
# starts with a letter and ends with a number
valid_attack_pattern = r'(^[a-z]{1})([0-9]$)'
user_input = raw_user_input.lower().strip()
if user_input == show:
return (UserActionType.SHOW, None)
match = re.search(valid_attack_pattern, user_input)
if match and match.lastindex == 2:
col = match.group(1)
row = match.group(2)
return (UserActionType.ATTACK, (col, row))
return (UserActionType.INVALID, None)
Now the GameCli class can just use this instead.
class GameCli:
'''Manages interactions between the user and the game.'''
def __init__(self, user_inputs):
self.user_inputs = user_inputs
def run(self):
'''Parses and applies user provided commands to the game'''
for user_input in self.user_inputs:
(action_type, action_info) = parse_user_input(user_input)
if action_type == UserActionType.SHOW:
self._display_board_layout()
elif action_type == UserActionType.ATTACK:
(col, row) = action_info
self._attack(col, row)
else:
self._display_invalid_user_input_message()
def _display_board_layout(self):
print('TODO: display board')
def _attack(self, col, row):
print('TODO: attack')
def _display_invalid_user_input_message(self):
print('TODO: display invalid message')
Parse Tests
Now let’s add a real test. I usually just like to start with the simplest one. Let’s start with “show“. We know if we pass this in as an input, the action should be a SHOW type.
from src.game_cli import parse_user_input, UserActionType
class TestParseUserInput(unittest.TestCase):
def test_show_returns_show_action(self):
(action_type, action_info) = parse_user_input('show')
self.assertEqual(action_type, UserActionType.SHOW)
self.assertEqual(action_info, None)
We can run and verify it works.
$ python3 -m unittest test/test* -v
test_show_returns_show_action (test.test_game_cli.TestParseUserInput) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Now let’s add the other two action types we expect.
class TestParseUserInput(unittest.TestCase):
def test_show_returns_show_action(self):
(action_type, action_info) = parse_user_input('show')
self.assertEqual(action_type, UserActionType.SHOW)
self.assertEqual(action_info, None)
def test_coordinate_returns_attack_action_and_coordinate(self):
(action_type, action_info) = parse_user_input('a8')
self.assertEqual(action_type, UserActionType.ATTACK)
expected_coordinate = ('a', '8')
self.assertEqual(action_info, expected_coordinate)
def test_invalid_input_returns_invalid_action(self):
(action_type, action_info) = parse_user_input('abcde')
self.assertEqual(action_type, UserActionType.INVALID)
self.assertEqual(action_info, None)
Great now looks like our tests are running and passing.
$ python3 -m unittest test/test* -v
test_coordinate_returns_attack_action_and_coordinate (test.test_game_cli.TestParseUserInput) ... ok
test_invalid_input_returns_invalid_action (test.test_game_cli.TestParseUserInput) ... ok
test_show_returns_show_action (test.test_game_cli.TestParseUserInput) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
Having a test suite is great because it not only saves you time but also if you make changes in the future, they can help verify that things are still working as expected.
Project changeset.
Leave a comment