From b8a0451059b84d20cfef87edcb471e060050d442 Mon Sep 17 00:00:00 2001
From: brodriguez8774 <brodriguez8774@gmail.com>
Date: Sun, 29 Sep 2019 13:53:50 -0400
Subject: [PATCH] Implement tests for UserInterface class, using mock input

---
 documents/references.md                       |  30 ++++-
 resources/interface.py                        |   9 +-
 resources/knapsack.py                         |   7 +-
 .../display_values.txt                        |  45 +++++++
 .../use_input_and_solve.txt                   | 113 ++++++++++++++++++
 .../use_json_and_solve.txt                    |  73 +++++++++++
 tests/{test.py => main.py}                    |   0
 tests/mock_user_input/display_values.txt      |   5 +
 tests/mock_user_input/use_input_and_solve.txt |  15 +++
 tests/mock_user_input/use_json_and_solve.txt  |   8 ++
 tests/resources/interface.py                  |  82 +++++++++++++
 11 files changed, 383 insertions(+), 4 deletions(-)
 create mode 100644 tests/expected_program_output/display_values.txt
 create mode 100644 tests/expected_program_output/use_input_and_solve.txt
 create mode 100644 tests/expected_program_output/use_json_and_solve.txt
 rename tests/{test.py => main.py} (100%)
 create mode 100644 tests/mock_user_input/display_values.txt
 create mode 100644 tests/mock_user_input/use_input_and_solve.txt
 create mode 100644 tests/mock_user_input/use_json_and_solve.txt
 create mode 100644 tests/resources/interface.py

diff --git a/documents/references.md b/documents/references.md
index 4585f73..2f7b0f3 100644
--- a/documents/references.md
+++ b/documents/references.md
@@ -20,9 +20,37 @@ Used to move to the "Script's Directory", regardless of where the script was lau
 ### Reading/Parsing JSON File with Python
 <https://linuxconfig.org/how-to-parse-data-from-json-into-python>
 
-## Padding Console Output in Python
+### Padding Console Output in Python
 <https://stackoverflow.com/a/5676884>
 
+### UnitTesting Subtests
+Using subtests: <https://www.caktusgroup.com/blog/2017/05/29/subtests-are-best/>
+
+### Testing the "resources/interface.py" File 
+We were told to have tests for everything, and our teacher linked us this <https://stackoverflow.com/a/36491341>.<br>
+Okay, cool. I implemented it and it was simple enough. Except I realized that I was running input methods through tests,
+but not actually testing anything.
+
+My next thought was "okay, the interface doesn't do anything but display output for the user. So I just need a way to
+read in stdout", which lead me to <https://stackoverflow.com/a/39320622>. This is great and works well enough for
+basic print() statements. ...but it doesn't seem to catch logging, which is what I use for my output.
+
+A good bit of research later, and I found <https://stackoverflow.com/a/34920727> which lead me to
+<https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertLogs>. They seem great in theory, but I
+couldn't figure out how to get them to work.
+
+While trying to troubleshoot the above two links, I stumbled upon <https://stackoverflow.com/a/53257669> and thought
+"hey, that looks a lot like my own logging setup". But alas, I couldn't determine how to get that to work with testing
+either (<https://stackoverflow.com/q/7472863> seems to imply it has to do with the UnitTest runner replacing
+stdout/stderr on initialization?).
+
+But that put an idea in my head. "Can I just temporarily replace logging with something that I know works, such as 
+the print() function?" And thus, my current implementation was born. `knapsack.py` and `interface.py` both have a
+`__init__` variable that simply replaces `logging.info` with `print`, if True. Doing so makes the second link (above) a
+valid solution, allowing me to read in predefined input, and compare results against expected output.
+
+It might not be the cleanest solution, but it works fine for this small of a project, and gives me a good place to start
+if I ever want to look into this further, in the future.
 
 ## Older Relevant References
 References linked in previous assignments.
diff --git a/resources/interface.py b/resources/interface.py
index f27e085..5ce498c 100644
--- a/resources/interface.py
+++ b/resources/interface.py
@@ -21,9 +21,14 @@ logger = init_logging.get_logger(__name__)
 
 
 class UserInterface():
-    def __init__(self):
+    def __init__(self, testing_output=False):
         self._run_program_bool_ = True
-        self._knapsack_algorithm = FractionalKnapsackAlgorithm()
+        self._knapsack_algorithm = FractionalKnapsackAlgorithm(testing_output)
+
+        # For catching output and comparing during testing.
+        # See documents/references.md for more info.
+        if testing_output:
+            logger.info = print
 
     def user_interface_loop(self):
         """
diff --git a/resources/knapsack.py b/resources/knapsack.py
index 1553d0b..9c748ea 100644
--- a/resources/knapsack.py
+++ b/resources/knapsack.py
@@ -24,12 +24,17 @@ class FractionalKnapsackAlgorithm():
     """
     Knapsack algorithm.
     """
-    def __init__(self):
+    def __init__(self, testing_output=False):
         self._item_set_ = []
         self._max_weight_ = 0
         self._items_populated_ = False
         self._weight_populated_ = False
 
+        # For catching output and comparing during testing.
+        # See documents/references.md for more info.
+        if testing_output:
+            logger.info = print
+
     def set_item_set(self, item_set):
         """
         Sets group of items to choose from.
diff --git a/tests/expected_program_output/display_values.txt b/tests/expected_program_output/display_values.txt
new file mode 100644
index 0000000..bf6e22c
--- /dev/null
+++ b/tests/expected_program_output/display_values.txt
@@ -0,0 +1,45 @@
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+Current Max Weight: 0
+Current Item Set: []
+
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+Current Max Weight: 0
+Current Item Set: []
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+Exiting Program
diff --git a/tests/expected_program_output/use_input_and_solve.txt b/tests/expected_program_output/use_input_and_solve.txt
new file mode 100644
index 0000000..1ff5a92
--- /dev/null
+++ b/tests/expected_program_output/use_input_and_solve.txt
@@ -0,0 +1,113 @@
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+Enter a positive integer for "Max Weight":
+"Max Weight" set to 4
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+Enter Item "Benefit" amount (must be a positive integer):
+Enter Item "Cost" amount (must be a positive integer):
+Successfully added new item.
+Current Item Set:
+================================================================
+| Item # | Total Benefit | Total Amount | Value Per One Weight |
+================================================================
+|      1 |            35 |            3 |   11.666666666666666 |
+================================================================
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+Enter Item "Benefit" amount (must be a positive integer):
+Enter Item "Cost" amount (must be a positive integer):
+Successfully added new item.
+Current Item Set:
+================================================================
+| Item # | Total Benefit | Total Amount | Value Per One Weight |
+================================================================
+|      1 |            35 |            3 |   11.666666666666666 |
+|      2 |            20 |            2 |                 10.0 |
+================================================================
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+Enter Item "Benefit" amount (must be a positive integer):
+Enter Item "Cost" amount (must be a positive integer):
+Successfully added new item.
+Current Item Set:
+================================================================
+| Item # | Total Benefit | Total Amount | Value Per One Weight |
+================================================================
+|      1 |            35 |            3 |   11.666666666666666 |
+|      2 |            20 |            2 |                 10.0 |
+|      3 |            20 |            2 |                 10.0 |
+================================================================
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+
+Current Max Weight: 4
+Current Item Set:
+===============================================================================
+| Item # | Total Benefit | Total Amount | Value Per One Weight | Amount Taken |
+===============================================================================
+|      1 |            35 |            3 |   11.666666666666666 |            3 |
+|      2 |            20 |            2 |                 10.0 |            1 |
+|      3 |            20 |            2 |                 10.0 |            0 |
+===============================================================================
+
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+Exiting Program
diff --git a/tests/expected_program_output/use_json_and_solve.txt b/tests/expected_program_output/use_json_and_solve.txt
new file mode 100644
index 0000000..c6b8034
--- /dev/null
+++ b/tests/expected_program_output/use_json_and_solve.txt
@@ -0,0 +1,73 @@
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+Enter a positive integer for "Max Weight":
+"Max Weight" set to 4
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+Enter a JSON formatted file to import from:
+Successfully set new Item Set from JSON data.
+Current Item Set:
+================================================================
+| Item # | Total Benefit | Total Amount | Value Per One Weight |
+================================================================
+|      1 |            35 |            3 |   11.666666666666666 |
+|      2 |            20 |            2 |                 10.0 |
+|      3 |            20 |            2 |                 10.0 |
+================================================================
+
+PROGRAM VALUES MENU
+Select an option:
+   1) Display current program values.
+   2) Set "Max Weight" value.
+   3) Set "Item Set" by JSON import.
+   4) Add value to "Item Set" manually.
+   5) Clear "Max Weight" value.
+   6) Clear entire "Item Set".
+   0) Back
+
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+
+Current Max Weight: 4
+Current Item Set:
+===============================================================================
+| Item # | Total Benefit | Total Amount | Value Per One Weight | Amount Taken |
+===============================================================================
+|      1 |            35 |            3 |   11.666666666666666 |            3 |
+|      2 |            20 |            2 |                 10.0 |            1 |
+|      3 |            20 |            2 |                 10.0 |            0 |
+===============================================================================
+
+MAIN MENU
+Select an option:
+   1) Display current program values.
+   2) Set program values.
+   3) Solve Knapsack Problem with current program values.
+   0) Exit program.
+Exiting Program
diff --git a/tests/test.py b/tests/main.py
similarity index 100%
rename from tests/test.py
rename to tests/main.py
diff --git a/tests/mock_user_input/display_values.txt b/tests/mock_user_input/display_values.txt
new file mode 100644
index 0000000..9a33115
--- /dev/null
+++ b/tests/mock_user_input/display_values.txt
@@ -0,0 +1,5 @@
+1
+2
+1
+0
+0
diff --git a/tests/mock_user_input/use_input_and_solve.txt b/tests/mock_user_input/use_input_and_solve.txt
new file mode 100644
index 0000000..6c0c7e2
--- /dev/null
+++ b/tests/mock_user_input/use_input_and_solve.txt
@@ -0,0 +1,15 @@
+2
+2
+4
+4
+35
+3
+4
+20
+2
+4
+20
+2
+0
+3
+0
diff --git a/tests/mock_user_input/use_json_and_solve.txt b/tests/mock_user_input/use_json_and_solve.txt
new file mode 100644
index 0000000..2f45ed8
--- /dev/null
+++ b/tests/mock_user_input/use_json_and_solve.txt
@@ -0,0 +1,8 @@
+2
+2
+4
+3
+example_item_set_2.json
+0
+3
+0
diff --git a/tests/resources/interface.py b/tests/resources/interface.py
new file mode 100644
index 0000000..080b4c3
--- /dev/null
+++ b/tests/resources/interface.py
@@ -0,0 +1,82 @@
+"""
+Date: 09-20-19
+Class: CS5310
+Assignment: Fractional Knapsack Problem
+Author: Brandon Rodriguez
+
+
+Tests for User Interface Class.
+"""
+
+# System Imports.
+import contextlib, sys, unittest
+from io import StringIO
+
+# User Class Imports.
+from resources.interface import UserInterface
+
+
+class TestUserInterface(unittest.TestCase):
+    def setUp(self):
+        self.interface = UserInterface(testing_output=True)
+        self.original_stdin = sys.stdin
+
+    def tearDown(self):
+        sys.stdin = self.original_stdin
+
+    def test_display_values(self):
+        # Redirect stdin and stdout.
+        sys.stdin = open('tests/mock_user_input/display_values.txt')
+        temp_stdout = StringIO()
+
+        # Call interface function.
+        with contextlib.redirect_stdout(temp_stdout):
+            self.interface.user_interface_loop()
+
+        # Verify output against file of expected output.
+        actual_output = temp_stdout.getvalue().strip()
+        output_file = open('tests/expected_program_output/display_values.txt')
+        expected_output = output_file.read().strip()
+        self.assertEqual(actual_output, expected_output)
+
+        # Close open files.
+        sys.stdin.close()
+        output_file.close()
+
+    def test_use_json_and_solve(self):
+        # Redirect stdin and stdout.
+        sys.stdin = open('tests/mock_user_input/use_json_and_solve.txt')
+        temp_stdout = StringIO()
+
+        # Call interface function.
+        with contextlib.redirect_stdout(temp_stdout):
+            self.interface.user_interface_loop()
+
+        # Verify output against file of expected output.
+        actual_output = temp_stdout.getvalue().strip()
+        output_file = open('tests/expected_program_output/use_json_and_solve.txt')
+        expected_output = output_file.read().strip()
+        self.assertEqual(actual_output, expected_output)
+
+        # Close open files.
+        sys.stdin.close()
+        output_file.close()
+
+    def test_use_input_and_solve(self):
+        # Redirect stdin and stdout.
+        sys.stdin = open('tests/mock_user_input/use_input_and_solve.txt')
+        temp_stdout = StringIO()
+
+        # Call interface function.
+        with contextlib.redirect_stdout(temp_stdout):
+            self.interface.user_interface_loop()
+
+        # Verify output against file of expected output.
+        actual_output = temp_stdout.getvalue().strip()
+        output_file = open('tests/expected_program_output/use_input_and_solve.txt')
+        expected_output = output_file.read().strip()
+        self.assertEqual(actual_output, expected_output)
+
+        # Close open files.
+        sys.stdin.close()
+        output_file.close()
-- 
GitLab