Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test displaying the resume text #48

Open
nicobrenner opened this issue Mar 15, 2024 · 2 comments
Open

Test displaying the resume text #48

nicobrenner opened this issue Mar 15, 2024 · 2 comments

Comments

@nicobrenner
Copy link
Owner

nicobrenner commented Mar 15, 2024

The goal is to have an additional test inside src/test_menu.py, that makes sure:

  • when a user selects the 'Edit Resume' option from the main menu (this option is the first option in the menu, but it only becomes available when the file in RESUME_PATH (eg. temp_test_resume.txt), exists and contains some text (the resume)
    • the resume text inside RESUME_PATH is displayed on screen
    • there are directions at the top of the screen ("Base Resume (Press 'q' to go back, 'r' to replace):")
    • when the user presses 'q'
      • the screen goes back to the menu

This can be done by creating a method test_displaying_resume(self, mock_getenv, mock_curses) to test the workflow above in the app

Let's check the code of the current test_manage_resume method as a base example. In there we find the following (open https://github.com/nicobrenner/commandjobs/blob/master/src/test_menu.py if you want to see the whole code while following along the sections below):

This first part:

  • defines the test method and
  • defines a mock method for environment variables, what this does is that when the app calls os.getenv('OPENAI_API_KEY'), instead of reading the environment variables from the terminal, it will get the value defined in the mock (in this case, 'test_key')
def test_manage_resume(self, mock_getenv, mock_curses):
# Mock environment variables
mock_getenv.side_effect = lambda x: {'OPENAI_API_KEY': 'test_key', 'BASE_RESUME_PATH': 'temp_base_resume.txt', 'HN_START_URL': 'test_url', 'COMMANDJOBS_LISTINGS_PER_BATCH': '10', 'OPENAI_GPT_MODEL': 'gpt-3.5'}.get(x, None)

Below, the same concept of creating a mock, but for ncurses and stdscr (which MainApp uses to create the user interface)

# Mock stdscr object
mock_stdscr = MagicMock()
mock_curses.initscr.return_value = mock_stdscr
mock_stdscr.getmaxyx.return_value = (100, 40)  # Example values for a terminal size

The following part checks if the test base resume exists, and if so, deletes it to have the right environment for the test

# This is testing when the resume file doesn't exist
# Remove test resume file, to make sure it doesn't exist
temp_test_resume_path = os.getenv('BASE_RESUME_PATH')
if os.path.exists(temp_test_resume_path):
     os.remove(temp_test_resume_path)

In the case of the test for displaying the resume, you should just directly write to the temp_test_resume_path some test text, then have the test go through the app and display the text. The code below reads a sample resume into a test_resume_text variable

# Use config/base_resume.sample as the test resume        
test_resume_text = ''
with open('config/base_resume.sample', 'r') as file:
     test_resume_text = file.read()

To simulate the input for the app, the input needs to be mocked. The code below can mock 2 different sequences of input, the input for getch, which is the functions that listens for keyboard input when the app is displaying the main menu, and the input for get_wch which supports utf8 and is the method that captures the resume text when the user pastes it into the terminal

These sequences of inputs will be consumed/run in order, from left to right, or from beginning to end

You'll notice the input sequence for getch is commented out and is there only as an example, given that the test will skip the main menu when calling the manage_resume() method directly. Hence this test only needs to simulate the get_wch input of pasting the resume text and then Esc at the end (['\x1b'])

# Mock user input sequence for getch and get_wch
# Presses Enter (10) to go into Paste Resume option
# mock_stdscr.getch.side_effect = [10]
        
# Paste the resume text + Esc ('\x1b'), to save the resume
mock_stdscr.get_wch.side_effect = list(test_resume_text) + ['\x1b']

Now here the test calls the part of the app that it wants to test. When menu.manage_resume() is called, that part of the application is executed and the application uses the mocked functions, including the mocked input. In this case, the method shows the interface for pasting the resume for the first time, and the test pastes the test resume text, then presses Esc and finally checks for the exit_message it gets back from manage_resume()

The exit_message should be "Resume saved to ..." and include the path of the test resume, and so the assertEqual() method checks that's the case, and it throws an error otherwise. The assert methods are what determines if the test passes or not. For a test to pass, all the assertions need to pass

# Simulate calling capture_text_with_scrolling
exit_message = menu.manage_resume(mock_stdscr)
        
# Verify we got a success message
self.assertEqual(exit_message, f'Resume saved to {temp_test_resume_path}')

There is a second assertion here, and it is checking that the saved text is the same as originally pasted into the application

# Verify the text was saved to base_resume.txt
with open(temp_test_resume_path, 'r') as file:
     saved_text = file.read()

self.assertEqual(saved_text, test_resume_text)

Finally, do some cleanup, removing temporary files used during the test

# Remove temp test resume file
if os.path.exists(temp_test_resume_path):
     os.remove(temp_test_resume_path)
        
temp_test_db_path = DB_PATH
if os.path.exists(temp_test_db_path):
     os.remove(temp_test_db_path)

You can run the current test with: python src/test_menu.py

I also recommend feeding this ticket + the code in src/test_menu.py + the code in src/menu.py to chatgpt and ask it to provide some guidance, or even some code that you can try out

@aaryamantriescode
Copy link

Thanks ill get on it !

@nicobrenner
Copy link
Owner Author

Here's a working test:

    @patch('menu.curses')
    @patch('menu.os.getenv')
    def test_displaying_resume(self, mock_getenv, mock_curses):
        # Mock environment variables
        mock_getenv.side_effect = lambda x: {'OPENAI_API_KEY': 'test_key', 'BASE_RESUME_PATH': 'temp_test_resume.txt', 'HN_START_URL': 'test_url', 'COMMANDJOBS_LISTINGS_PER_BATCH': '10', 'OPENAI_GPT_MODEL': 'gpt-3.5'}.get(x, None)
    
        # Mock stdscr object
        mock_stdscr = MagicMock()
        mock_curses.initscr.return_value = mock_stdscr
        mock_stdscr.getmaxyx.return_value = (100, 40)  # Example values for a terminal size

        # Use config/base_resume.sample as the test resume        
        test_resume_text = ''
        with open('config/base_resume.sample', 'r') as file:
            test_resume_text = file.read()

        # Save the test resume text to the temporary resume file
        with open('temp_test_resume.txt', 'w') as file:
            file.write(test_resume_text)

        # Mock user input sequence for getch
        mock_stdscr.getch.side_effect = [ord('q')]  # Press 'q' to exit

        # Initialize Menu with mocked stdscr and logger
        logger = MagicMock()
        with patch.object(MenuApp, 'run', return_value=None):
            menu = MenuApp(mock_stdscr, logger)
            
        # Assert that the displayed resume text matches the test resume text

        # Call the method being tested
        # Right before calling manage_resume, reset mock_stdscr's call list
        mock_stdscr.reset_mock()  # This clears the previous calls
        exit_message = menu.manage_resume(mock_stdscr)
        
        # Verify we got a success message
        self.assertEqual(exit_message, 'Resume not updated')
    
        # Verify that the displayed resume text matches the test resume text
        # Getting the text that should have been displayed on screen
        screen_contents = ''
        previous_call = ""
        for call in mock_stdscr.method_calls:
            call_str = str(call)
            if call_str.startswith("call.addstr"):
                text = call.args[-1] if len(call.args) == 3 else call.args[0]
                screen_contents += text
                previous_call = call_str
                
        # Check if the captured screen contents include the expected resume text
        self.assertIn(test_resume_text.strip(), screen_contents.strip())

        # Remove the temporary resume file
        if os.path.exists('temp_test_resume.txt'):
            os.remove('temp_test_resume.txt')

That test found a slight bug or quirk in the code:

# Line 257 of src/menu.py
# stripping the lines for displaying them on the screen is ok
# but since that modifies the original text, then what is displayed
# on the screen doesn't match the original text
# removing the .strip() from line solves this
for i, line in enumerate(lines[offset:offset+max_y-5]):
                # self.stdscr.addstr(i+3, 0, line.strip())
                self.stdscr.addstr(i+3, 0, line)

aaryamantriescode added a commit to aaryamantriescode/commandjobsnew that referenced this issue Mar 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants