1

I am developing in python. My code calls terminal commands. I want to make unit tests for those functions. How do I test if terminal commands are working properly? Also I establish a bluetooth connection with gatt commands. How do I test functions building upon that?


    import pexpect
    
    MAC = "00:80:41:AE:fD:7C"
    
    def connect():
        gatt = pexpect.spawn("sudo gatttool -I")
        gatt.sendline("connect " + MAC)
        gatt.expect("Connection successful")
    def send(x):
        connect()
        gatt.sendline( f"char-write-req 0x000c {x}0" )
        gatt.expect("Characteristic value was written successfully")
    
    send("ff")

2 Answers 2

0

What do you want to test?

If the answer is that you want to test that your code interacts with gatttool in the way you expect, then just try it.

If the answer is that you want to test whether you're using pexpect correctly, then I suggest that you write an external program that just records what you send to it and replies with some text that you expect (probably the responses expected from gatttool). Then run that instead of gatttool in your tests. For example, you can make your connect function accept parameters for the strings to pass to pexpect functions - different things for your external test program or for gatttool.

Even if you are using pexpect correctly and you can successfully interact with gatttool, you should consider what happens if gatttool responds in an unexpected way. Your connection to it might drop, or it might respond in a different way to normal. A test external program would also let you test that something sensible happens in those cases.

Sign up to request clarification or add additional context in comments.

Comments

0

Does this help you?

import unittest
from unittest.mock import MagicMock, patch
class TestGatttoolInteraction(unittest.TestCase):
    """
    Test suite for the gatttool interaction functions.
    Mocks pexpect.spawn to avoid actual external process calls.
    """

    @patch('pexpect.spawn') # Patch pexpect.spawn globally for all tests in this class
    def test_connect_success(self, mock_spawn):
        """
        Tests that the connect function correctly spawns gatttool,
        sends the connect command, and expects success.
        """
        # Configure the mock pexpect.spawn object.
        # When mock_spawn() is called, it returns a mock object (mock_gatt).
        mock_gatt = MagicMock()
        mock_spawn.return_value = mock_gatt

        # Simulate gatttool's response to the connect command.
        # When mock_gatt.expect is called, we want it to succeed without error.
        # We don't need to specify return value for expect if it just needs to not raise.
        mock_gatt.expect.return_value = 0 # A common return for success in pexpect

        # Call the function under test
        connect()

        # Assertions to verify correct behavior:

        # 1. Verify pexpect.spawn was called with the correct command
        mock_spawn.assert_called_once_with("sudo gatttool -I")

        # 2. Verify the correct connect command was sent
        mock_gatt.sendline.assert_any_call("connect " + MAC)

        # 3. Verify 'Connection successful' was expected
        mock_gatt.expect.assert_called_once_with("Connection successful")

        # 4. Verify the global 'gatt' variable was set to the mock object
        self.assertIs(gatt, mock_gatt)


    @patch('pexpect.spawn')
    def test_connect_failure(self, mock_spawn):
        """
        Tests that connect raises an exception if 'Connection successful'
        is not found, simulating a connection failure.
        """
        mock_gatt = MagicMock()
        mock_spawn.return_value = mock_gatt

        # Configure expect to raise an exception, simulating failure to find expected text
        mock_gatt.expect.side_effect = pexpect.exceptions.TIMEOUT('Timeout waiting for "Connection successful"')

        # Assert that the function raises the expected pexpect exception
        with self.assertRaises(pexpect.exceptions.TIMEOUT):
            connect()

        mock_spawn.assert_called_once_with("sudo gatttool -I")
        mock_gatt.sendline.assert_called_once_with("connect " + MAC)
        mock_gatt.expect.assert_called_once_with("Connection successful")


    @patch('pexpect.spawn') # Patch pexpect.spawn for this test
    def test_send_success(self, mock_spawn):
        """
        Tests that the send function correctly calls connect(),
        sends the char-write-req command, and expects success.
        """
        mock_gatt_instance = MagicMock()
        # Ensure that each call to pexpect.spawn() (e.g., from connect())
        # returns the same mock object in this test context.
        mock_spawn.return_value = mock_gatt_instance

        # Simulate successful responses for both connect() and send() operations
        # expect() should not raise an error
        mock_gatt_instance.expect.return_value = 0

        test_value = "ab"
        send(test_value)

        # Assertions:

        # 1. Verify pexpect.spawn was called twice (once by connect() inside send(), then again by connect() in the second call)
        # However, because send() calls connect() which *re-spawns* and overwrites 'gatt',
        # we only care about the state after the final connect().
        # The important thing is that gatt.sendline and gatt.expect are called correctly on the *final* gatt object.
        # Since connect() is called, pexpect.spawn will be called once.
        mock_spawn.assert_called_once_with("sudo gatttool -I")

        # 2. Verify the connect command was sent by connect()
        mock_gatt_instance.sendline.assert_any_call("connect " + MAC)

        # 3. Verify 'Connection successful' was expected by connect()
        mock_gatt_instance.expect.assert_any_call("Connection successful")

        # 4. Verify the characteristic write command was sent
        expected_write_command = f"char-write-req 0x000c {test_value}0"
        mock_gatt_instance.sendline.assert_any_call(expected_write_command)

        # 5. Verify 'Characteristic value was written successfully' was expected
        mock_gatt_instance.expect.assert_any_call("Characteristic value was written successfully")

        # Ensure gatt.sendline and gatt.expect were called for both operations
        # We use call_count to ensure both connect and send operations occurred on the mock
        self.assertEqual(mock_gatt_instance.sendline.call_count, 2)
        self.assertEqual(mock_gatt_instance.expect.call_count, 2)

    @patch('pexpect.spawn')
    def test_send_write_failure(self, mock_spawn):
        """
        Tests that send raises an exception if 'Characteristic value was written successfully'
        is not found, simulating a write failure.
        """
        mock_gatt_instance = MagicMock()
        mock_spawn.return_value = mock_gatt_instance

        # Set up expect to succeed for the 'connect' call
        mock_gatt_instance.expect.side_effect = [
            0, # Success for "Connection successful"
            pexpect.exceptions.TIMEOUT('Timeout waiting for "Characteristic value was written successfully"') # Failure for the write
        ]

        test_value = "1a"
        with self.assertRaises(pexpect.exceptions.TIMEOUT):
            send(test_value)

        mock_spawn.assert_called_once_with("sudo gatttool -I")
        mock_gatt_instance.sendline.assert_any_call("connect " + MAC)
        mock_gatt_instance.expect.assert_any_call("Connection successful")
        mock_gatt_instance.sendline.assert_any_call(f"char-write-req 0x000c {test_value}0")
        mock_gatt_instance.expect.assert_any_call("Characteristic value was written successfully")
        self.assertEqual(mock_gatt_instance.sendline.call_count, 2)
        self.assertEqual(mock_gatt_instance.expect.call_count, 2)


if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False) # exit=False prevents sys.exit()

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.