Why does python not have a mechanism for data hiding?

David wizzardx at gmail.com
Thu Jun 5 16:21:07 EDT 2008


On Wed, Jun 4, 2008 at 2:54 PM, Roy Smith <roy at panix.com> wrote:
> In article <87y75llp5x.fsf at benfinney.id.au>,
>  Ben Finney <bignose+hates-spam at benfinney.id.au> wrote:
>
>> By definition, "private" functions are not part of the publicly
>> documented behaviour of the unit. Any behaviour exhibited by some
>> private component is seen externally as a behaviour of some public
>> component.
>
> You know the difference between theory and reality?  In theory, there is
> none...  Sometimes it's useful to test internal components.  Imagine this
> class:
>
> class ArmegeddonMachine:
>   def pushTheButton(self):
>      "Destroy a random city"
>      city = self._pickCity()
>      self._destroy(city)
>
>   def _pickCity():
>      cities = ['New York', 'Moscow', 'Tokyo', 'Beijing', 'Mumbai']
>      thePoorSchmucks = random.choice(cities)
>      return 'New York'
>
>   def _destroy(self, city):
>      missle = ICBM()
>      missle.aim(city)
>      missle.launch()
>
> The only externally visible interface is pushTheButton(), yet you don't
> really want to call that during testing.  What you do want to do is test
> that a random city really does get picked.
>
> You can do one of two things at this point.  You can say, "But, that's not
> part of the externally visible interface" and refuse to test it, or you can
> figure out a way to test it.  Up to you.
> --
> http://mail.python.org/mailman/listinfo/python-list
>

Sorry for the long post in advance. I'm busy studying unit testing、and
here is how I would go about testing the above code.

1) Make the code more testable through public (not private)
interfaces. ie, split out the functionality into more public, but
still properly encapsulated forms (modules/classes, etc). Don't need
to make everything public, just enough that it's testable without
digging into privates attributes, but still well-encapsulated.

2) Link the split-up logic together (eg, in class constructors). Use
public attributes for this. Or use private attributes, but make them
updatable through public methods.

3) In the unit tests update the object being tested, so it uses mock
objects instead of the ones it uses by default (aka dependency
injection).

4) Run the methods to be tested, and check that the mock objects were
updated correctly.

Here is an example (very rough, untested & incomplete):

# Updated armageddonmachine module:

class CityPicker:
   def pick(self):
      cities = ['New York', 'Moscow', 'Tokyo', 'Beijing', 'Mumbai']
      thePoorSchmucks = random.choice(cities)
      return 'New York' # Your bug is still here

class Destroyer:
   def destroy(self, city):
      missle = ICBM()
      missle.aim(city)
      missle.launch()

class ArmegeddonMachine:
   def __init__(self):
      self.city_picker = CityPicker()
      self.destroyer = Destroyer()

   def pushTheButton(self):
      "Destroy a random city"
      city = self.city_picker.pick()
      self.destroyer.destroy(city)

# unit test module for armageddonmachine
# Only has tests for CityPicker and ArmageddonMachine

import armageddonmachine

# Unit test code for CityPicker


# -- Mock objects --


class MockRandom:
   def __init__(self):
      self.choose = None
   def choice(self, list_):
      assert self.choose is not None
     return list_[self.choose]


class TestCityPicker:
   def setup()
      # Setup code run before each unit test in this class
      self.city_picker = CityPicker()
      self._bkp_random = armaggedonmachine.random

   def teardown()
      # Teardown  code run after each unit test in this class
      armaggedonmachine.random = self._bkp_random

   def test_should_pick_random_cities_correctly(self):
      armaggedonmachine.random = MockRandom()

      armaggedonmachine.choose = 0
      assert city_picker.pick() == 'New York'

      armaggedonmachine.choose = 1
      assert city_picker.pick() == 'Moscow' # This will catch your bug

      armaggedonmachine.choose = 2
      assert city_picker.pick() == 'Tokyo'

      armaggedonmachine.choose = 3
      assert city_picker.pick() == 'Beijing'

      armaggedonmachine.choose = 4
      assert city_picker.pick() == 'Mumbai'

# Unit test code for ArmageddonMachine

# -- Mock Classes --


class MockCityPicker:
  def __init__(self):
    self.city_to_pick = "Test City"

  def pick(self):
    return self.city_to_pick


class MockDestroyer:
  def __init__(self):
     self.destroyed_cities = set()

  def destroy(self, city):
     self.destroyed_cities.append(city)


# -- Unit Tests

class TestArmageddonMachine:

  def setup(self):
    # Setup code that runs before each unit test in this class
    self.machine = ArmageddonMachine()
    self._bkp_picker = self.machine.city_picker
    self._bkp_destroyer = self.machine.destroyer
    self.machine.city_picker = MockCityPicker()
    self.machine.destroyer = MockCityDestroyer()

  def test_should_destroy_a_city(self):
     self.machine.pushTheButton()
     assert self.machine.destroyer.destroyed_cities == "Test City"



More information about the Python-list mailing list