“Codebase Cleanup” Exercise
In this exercise we will practice refactoring and testing Python code. Primary topics in focus include: refactoring code using custom functions, organizing code using the “main conditional” (a Python convention which allows certain code to be imported from a file, without running other parts of the file), and implementing automated tests for those functions.
Setup
To setup this exercise, bring up an existing repository, or create a new repository called “codebase-cleanup”. Navigate there from the command line, and open the repository using your text editor.
Inside the repository, create subdirectories called “app” and “test”, respectively.
Also create and/or activate a project-specific virtual environment, as necessary.
Starter Code
Create a file in the “app” directory called “rps.py”, and place inside the following contents, and save the file:
# the "app/rps.py" file (v1)...
#
# USER SELECTION
#
u = input("Please choose one of 'Rock', 'Paper', or 'Scissors': ").lower()
print("USER CHOICE:", u)
if u not in ["rock", "paper", "scissors"]:
print("OOPS, TRY AGAIN")
exit()
#
# COMPUTER SELECTION
#
from random import choice
c = choice(["rock", "paper", "scissors"])
print("COMPUTER CHOICE:", c)
#
# DETERMINATION OF WINNER
#
if u == "rock" and c == "rock":
print("TIE GAME")
elif u == "rock" and c == "paper":
print("COMPUTER WINS")
elif u == "rock" and c == "scissors":
print("USER WINS")
elif u == "paper" and c == "rock":
print("COMPUTER WINS") # OOPS
elif u == "paper" and c == "paper":
print("TIE GAME")
elif u == "paper" and c == "scissors":
print("USER WINS") # OOPS
elif u == "scissors" and c == "rock":
print("COMPUTER WINS")
elif u == "scissors" and c == "paper":
print("USER WINS")
elif u == "scissors" and c == "scissors":
print("TIE GAME")NOTE: this code intentionally has a bug in the winner determination logic, which we will fix later.
After saving the file, run the file from the command line to play the game, choosing “rock” when prompted:
python app/game.py
# alternatively:
python -m app.gameEverything probably looks good so far.
If using version control, make a commit before moving on.
Maintenance Challenges
After releasing the code to end users, you start to receive some bug report messages saying “Sometimes the program gets the winner wrong!”.
Your objective is to revise the Python code to improve maintainability, and fix the bug. Along the way, take opportunities to improve the code’s style, and remove duplication. Also refactor, test, and document the winner determination logic. Optionally reduce complexity in the winner determination logic. If you have time, extend the logic by creating a GUI or web-based version of the game.
See the solutions file for solutions to the challenges below.
Syntax and Style
Let’s conform to some of the more important Python syntax and style guidelines.
Challenge 1: Move any import statements to the top of the file.
Removing Duplication
Let’s remove duplication in the code, and adhere to the DRY Principle.
Challenge 2: Refactor duplicate code related to the valid options ["rock", "paper", "scissors"] into a constant called VALID_OPTIONS. This removes duplication and adheres to the DRY principle.
Challenge 3: Consider refactoring duplicate outcome messages into constants called USER_WINS_MESSAGE, COMPUTER_WINS_MESSAGE, and TIE_GAME_MESSAGE. After doing so, if we want to change these messages later, we only have to change them in one spot.
Refactoring
We received some reports the winner determination logic is inaccurate. It may be hard to see where the bug is just by looking at the code. We will probably want to test the code, but before we do we will need to refactor it into a custom function that can be tested in isolation. Refactoring using a custom function will also enable us to extend this functionality later.
Challenge 4: Refactor the winner determination logic into a stand-alone function called determine_outcome. The function should accept the user choice and the computer choice as input parameters (perhaps called u and c), and should return a string denoting the outcome (i.e. “USER WINS” or “COMPUTER WINS”, or “TIE GAME”). Run the file again to see it still works.
NOTE: To be tested in an automated way later, the function should not ask the user for any inputs, as there are no users available when running tests in an automated way. Any user inputs should be passed into the function as parameters.
Challenge 5: Refactor the “rps.py” file to include the “main conditional”, which will allow us to import code cleanly from this file. Run the file again to see it still works.
Automated Testing
How do we know the function works? Let’s test it in an automated way.
Challenge 6: Implement an automated test to ensure the winner determination logic produces accurate outcomes, given all possible combinations of the inputs.
Adding a new “rps_test.py” file to the “test” directory, with the following contents:
# this is the "test/rps_test.py" file...
from app.rps import determine_outcome
def test_winners():
# tests for all edge cases:
assert determine_outcome(u="rock", c="rock") == "TIE GAME"
assert determine_outcome(u="rock", c="paper") == "COMPUTER WINS"
assert determine_outcome(u="rock", c="scissors") == "USER WINS"
assert determine_outcome(u="paper", c="rock") == "USER WINS"
assert determine_outcome(u="paper", c="paper") == "TIE GAME"
assert determine_outcome(u="paper", c="scissors") == "COMPUTER WINS"
assert determine_outcome(u="scissors", c="scissors") == "TIE"
assert determine_outcome(u="scissors", c="paper") == "WIN"
assert determine_outcome(u="scissors", c="rock") == "LOSE"Install the pytest package using pip, ideally adding it to the repository’s “requirements.txt” file and installing it from there:
#pip install pytest
pip install -r requirements.txtRun tests with pytest package:
pytestNotice the tests may be failing for now, until we fix the bug.
Improving Quality / Fixing Bugs
Let’s actually improve the functionality and fix the bug.
Challenge 7: Notice the existing winner determination logic produces some inaccurate outcomes, and update the logic to produce accurate outcomes.
If we have written our tests, we will have seen them failing, but after fixing the logic, the tests should now pass.
Improving Documentation
Since we have created a new custom function, we might as well take a moment to document it, to make it more understandable for others.
Challenge 8: Document the new winner determination function, using a docstring. Include a description of the function’s responsibilities, the parameters it accepts, and the values it returns. Consider optionally formatting your docstring using google-style or numpy-style docstring syntax.
NOTE: if you need help constructing a docstring using the desired formats, provide the function code to ChatGPT and ask it to create a google-style or numpy-style docstring for you 🤖
Reducing Complexity
Optionally integrate your repository with Code Climate, and perform a code complexity assessment. Notice Code Climate says the existing winner determination logic is “complex”.
Challenge 9: Refactor the complex logic to use less computational steps. HINT: you can use a nested if statement, or better yet a nested dictionary to model the winners.
See the complexity solutions file for multiple options, and discuss the complexity of each.
Enabling Extensibility
Suppose we want to keep the command-line game, but implement a graphical user interface (GUI) or web-based interface into the game, using a separate file.
Challenge 10. Create a new file called “rps_web.py”, to implement a web-based version of the game. For creating web applications, we will use the flask package.
See the web app solutions file for some solution code to make a web application.