1

I am trying to design and test code similar to the following in a good object oriented way? (Or a pythonic way?)

Here is a factory class which decides whether a person's name is long or short:

class NameLengthEvaluator(object):
    def __init__(self, cutoff=10)
        self.cutoff = cutoff

    def evaluate(self, name):
        if len(self.name) > cutoff:
            return 'long'
        else:
            return 'short'

Here is a person class with an opinion on the length of their own name:

 class Person(object):
     def __init__(self, name=None, long_name_opinion=8):
         self.name = name

     def name_length_opinion(self):
         return 'My names is ' + \
                    NameLengthEvaluator(long_name_opinion).evaluate(self.name)

A couple questions:

  • Does the Person method name_length_opinion() deserve a unit test, and if so what would it look like?
  • In general, is there a good way to test simple methods of classes with functionality that is entirely external?

It seems like any test for this method would just restate its implementation, and that the test would just exist to confirm that nobody was touching the code.

(disclaimer: code is untested and I am new to python)

zastrowm
  • 8,017
  • 3
  • 43
  • 63
Paul Orland
  • 542
  • 4
  • 16

3 Answers3

2

Unit Testing

Does the Person method name_length_opinion() deserve a unit test, and if so what would it look like?

Do you want to make sure it does what you think it does and makes sure it doesn't break in the future? If so, write a unit test for it.

and that the test would just exist to confirm that nobody was touching the code

Unit testing is more about making sure a class conforms to the contract that it specifies. You don't have to write a unit test for everything, but if it's a simple method, it should be a simple unit test anyways.

Repetition

It seems like any test for this method would just restate its implementation

You shouldn't be repeating the algorithm, you should be using use cases. For instance, a NameLengthEvaluator with a cutoff of 10 should have these be short names:

  • George
  • Mary

and these be long names:

  • MackTheKnife
  • JackTheRipper

So you should verify that the method reports the shortness of these names correctly. You should also test that a NameLengthEvaluator with a cutoff of 4 would report Mary as short and the others as long.

Throwaway Code?

If you've ever written a class and then written a main method that just runs the class to make sure it does what it is supposed to (and then you throw that main method away when you move onto another class), you've already written a unit test. But instead of throwing away, save it and convert it to a unit test so that in the future you can make sure you didn't break anything.

External Code

In general, is there a good way to test simple methods of classes with functionality that is entirely external

Well, if it's entirely external then why is it a method on that class? Normally you have at least some logic that can be tested. In this case, you can test that name_length_opinion returns My names is long or My names is short in the correct cases.

zastrowm
  • 8,017
  • 3
  • 43
  • 63
1

It really depends on the lifecycle of that code. It's obvious that, in its current state, the method is obviously correct, and the unit test is more of a specification for how it should behave. If you plan on making changes in the future (reimplementing NameLengthEvaluator somehow differently, for instance), having unit tests is great, because running your tests will catch any regressions. But in this case, it seems unlikely that you'd make any changes, so the tests are probably excessive (though a good sanity check).

Rafe Kettler
  • 75,757
  • 21
  • 156
  • 151
0

Normally you'd use a mock here. You could make a mock NameLengthEvaluator that returned an object that recorded what it was concatenated with, and when name_length_opinion returned, you'd check to make sure that it was used and concatenated with the right thing.

For example, using unittest.mock:

from unittest.mock import MagicMock, patch

@patch('your_module.NameLengthEvaluator', autospec=True)
def test_person_name_length_opinion(NameLengthEvaluator):
    expected_result = object()
    opinion = MagicMock(name='opinion')
    opinion.__radd__.return_value = expected_result
    name_length_evaluator = MagicMock(name='name_length_evaluator')
    name_length_evaluator.evaluate.return_value = opinion
    NameLengthEvaluator.return_value = name_length_evaluator

    name = object()
    length_limit = object()

    person = Person(name, long_name_opinion=length_limit)
    result = person.name_length_opinion()

    NameLengthEvaluator.assert_called_with(length_limit)
    name_length_evaluator.evaluate.assert_called_with(name)
    opinion.__radd__.assert_called_with('My names is ')
    assert result is expected_result

However, since the method is so simple, I'm not sure you care that much.

icktoofay
  • 126,289
  • 21
  • 250
  • 231