3
\$\begingroup\$

Recently I read about test-driven-design and decided to try to create a simple unit testing class to begin using this approach. I have not been programming in Python for very long so forgive my ignorance of the language.

I would like to discuss better approaches to handling multi-parameter test functions. Right now I accept a tuple (of varying length) with one or more function inputs.

I find the tuple input and "length" checks (lines 10-13) to be clunky. Is there a better way to handle this? I tried using a spread operator to distribute the arguments into the test function but this fails for list inputs (it spreads the list input itself rather than inserting it as the argument).

Like I said this is very basic and I'm sure there are modules out there I can import to do a much more thorough job. I learn best when I try something myself so while I appreciate any suggestions for external modules I'd like to focus on improving this class itself.

class Unit_Tester:
  def __init__(self, function, tests):
    self.function_to_test = function
    self.test_list = tests

  def unit_test(self, function_to_test, test_input, expected_output):
      try:
          number_of_inputs = len(test_input)
          if number_of_inputs == 1: function_output = function_to_test(test_input)
          elif number_of_inputs == 2: function_output = function_to_test(test_input[0], test_input[1])
          elif number_of_inputs == 3: function_output = function_to_test(test_input[0], test_input[1], test_input[2])
          elif type(test_input) == list: function_output = function_to_test(test_input)
      except Exception as error:
          print("Error occurred with test input: [{0}] value: {1}\nError Message: {2}\nCorrect the error and try again.\n"
          .format(type(test_input), test_input, error))
      else:
        try:
            assert function_output == expected_output
            print(self.unit_test_response(True, test_input, function_output, expected_output))
        except AssertionError:
            print(self.unit_test_response(False, test_input, function_output, expected_output))

  def unit_test_response(self, correct, test_input, function_output, expected_output): 
      score = "Test Passed" if correct else "Test Failed" 
      return "{0}\nInput: {1}\nExpected Output: {2}\nFunction Output: {3}\n".format(score, test_input, expected_output, function_output)

  def run_unit_tests(self, function_to_test, test_list):  
      for test_tuple in test_list:
          test_input, expected_output = test_tuple
          self.unit_test(function_to_test, test_input, expected_output)

  def run(self):
    self.run_unit_tests(self.function_to_test, self.test_list)
\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! I have removed part of your question, because asking for feature enhancements goes beyond the scope of Code Review. The good news is, hunting for deficiencies is an implicit part of every question; you don't need to request it. As for your directory structure question, that belongs on Stack Overflow. Post a question there explaining how you would like it to behave, and how it fails to meet your expectations. \$\endgroup\$ – 200_success Oct 30 '17 at 22:48
  • 1
    \$\begingroup\$ @200_success great news. and thanks for teaching me a new word with that tag "variadic". Sometimes its hard to form the right questions being self taught because I'm missing the technical terminology that you experts have access to! \$\endgroup\$ – vampiire Oct 30 '17 at 22:51
2
\$\begingroup\$

Since indentation is crucial in Python, you should consistently use four spaces, as specified in PEP 8, the official Python style guide.

Also, don't mix naming styles. Based on PEP 8, the class should be named UnitTester.


Your test harness neglects to support…

  • calling functions with no parameters, or with more than 3 parameters. This is easily solved using the * operator:

    function_output = function_to_test(*test_input)
    
  • calling functions with keyword parameters. You could accomplish this using the ** operator.

  • testing that a function raises an exception as expected. That is left as an exercise for you.

  • testing whether a function performs expected side-effects, such as printing certain output. This is quite tricky to implement, but it's something that doctest can easily handle.

    def aloha():
        """
        >>> aloha()
        Hello
        'Goodbye'
        """
        print('Hello')
        return "Goodbye"
    

This code is an abuse of assertions:

    try:
        assert function_output == expected_output
        print(self.unit_test_response(True, test_input, function_output, expected_output))
    except AssertionError:
        print(self.unit_test_response(False, test_input, function_output, expected_output))

Assertions are a way for you, as a programmer, to state that a certain condition must logically be true, not that you hope for a condition to be true.

Furthermore, assertions can be disabled, in which case every test would succeed!

You should replace that code with:

      result = (function_output == expected_output)
      print(self.unit_test_response(result, test_input, function_output, expected_output))

My biggest complaint about this unit-testing class, though, is that it is unclear how it is meant to be used. You didn't describe the intended usage in the question, and you didn't write docstrings like you should have. I'm guessing that this is your intended usage:

Unit_Tester(max, [
    ([3, 1, 4], 4),
    ([-3, -1, -4], -1),
]).run()

But why does the run_unit_tests(…) method exist? Is this a "valid" way to use your harness?

Unit_Tester(None, None).run_unit_tests(max, [
    ([3, 1, 4], 4),
    ([-3, -1, -4], -1),
])

If it is, then I recommend getting rid of the tests parameter in the constructor. If it isn't, then I recommend merging run_unit_tests() into run(), so that misuse is impossible.

Alternatively, name your "private" methods with a leading underscore (e.g. _unit_test_response(…) to indicate that a method should not be called directly.

\$\endgroup\$
  • \$\begingroup\$ Wow thank you for taking the time to give me such great instruction. I have rewritten the class and linked it below. I still however have one question remaining: How can I effectively accept any number (0+) of arguments in the test function? Your suggestion of using the * (unpacking) operator was actually the first that I tried. It seems to break when any of the arguments themselves are lists or tuples. Is there a way around this? I will have to do more research on handling expected exceptions. I had a nice chuckle at "This is an abuse of exceptions". Strong language but duly noted \$\endgroup\$ – vampiire Oct 31 '17 at 8:53
  • \$\begingroup\$ here is the updated class: gist.github.com/the-vampiire/6b42c6daa8c3bc1fa2d1a22e63b8835a \$\endgroup\$ – vampiire Oct 31 '17 at 8:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.