[tor-commits] [stem/master] Moving tutorial tests to be unit tests

atagar at torproject.org atagar at torproject.org
Mon Oct 15 16:17:05 UTC 2012


commit eb28305879647bd628794674517da4377c127819
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Oct 14 13:20:16 2012 -0700

    Moving tutorial tests to be unit tests
    
    Both of our integ tests for the tutorial examples were a bit clunky, the
    controller test because it could only run with newish tor versions and the
    descriptor test because it had to make use of metrics descriptors. Oh, and they
    didn't actually assert anything.
    
    Moving the tests to be unit tests instead. This included greatly expanding our
    mocking capabilities, supporting arbitrary mock objects. I'm not sure if the
    end result is any better than our prior integ tests since we're mocking pretty
    much everything (and in the case of the descriptor test it's pretty ugly). On
    the other hand the tutorial tests are primarily for basic syntax and
    compatability with our current version of stem so guess it's ok...
    
    All this said, I *really* like our arbitrary object mocking capability...
---
 docs/index.rst                             |    1 +
 run_tests.py                               |    2 +
 test/__init__.py                           |    1 +
 test/integ/control/controller.py           |   25 ---------
 test/integ/descriptor/server_descriptor.py |   29 ----------
 test/mocking.py                            |   38 +++++++++++++-
 test/unit/tutorial.py                      |   80 ++++++++++++++++++++++++++++
 7 files changed, 121 insertions(+), 55 deletions(-)

diff --git a/docs/index.rst b/docs/index.rst
index d6d7f01..35806d2 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -52,6 +52,7 @@ You'll need to restart Tor or issue a SIGHUP for these new settings to take effe
   bytes_written = controller.get_info("traffic/written")
   
   print "My Tor relay has read %s bytes and written %s." % (bytes_read, bytes_written)
+  controller.close()
 
 ::
 
diff --git a/run_tests.py b/run_tests.py
index ef172bf..c891bf6 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -42,6 +42,7 @@ import test.unit.util.tor_tools
 import test.unit.exit_policy.policy
 import test.unit.exit_policy.rule
 import test.unit.version
+import test.unit.tutorial
 import test.integ.connection.authentication
 import test.integ.connection.connect
 import test.integ.control.base_controller
@@ -127,6 +128,7 @@ UNIT_TESTS = (
   test.unit.exit_policy.rule.TestExitPolicyRule,
   test.unit.exit_policy.policy.TestExitPolicy,
   test.unit.version.TestVersion,
+  test.unit.tutorial.TestTutorial,
   test.unit.response.control_message.TestControlMessage,
   test.unit.response.control_line.TestControlLine,
   test.unit.response.getinfo.TestGetInfoResponse,
diff --git a/test/__init__.py b/test/__init__.py
index e46822c..e7add66 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -8,6 +8,7 @@ __all__ = [
   "output",
   "prompt",
   "runner",
+  "tutorial",
   "utils",
 ]
 
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index 36ac14a..bd17703 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -18,31 +18,6 @@ import test.runner
 import test.util
 
 class TestController(unittest.TestCase):
-  def test_tutorial(self):
-    """
-    Tests the tutorial from our front page.
-    """
-    
-    if test.runner.require_control(self): return
-    if test.runner.require_version(self, stem.version.Version("0.2.3.1")): return # uses a relatively new feature
-    elif not test.runner.Torrc.PORT in test.runner.get_runner().get_options():
-      test.runner.skip(self, "no port")
-      return
-    
-    from stem.control import Controller
-    
-    controller = Controller.from_port(control_port = test.runner.CONTROL_PORT)
-    
-    try:
-      controller.authenticate(test.runner.CONTROL_PASSWORD)
-      
-      bytes_read = controller.get_info("traffic/read")
-      bytes_written = controller.get_info("traffic/written")
-      
-      printed_msg = "My Tor relay has read %s bytes and written %s." % (bytes_read, bytes_written)
-    finally:
-      controller.close()
-  
   def test_from_port(self):
     """
     Basic sanity check for the from_port constructor.
diff --git a/test/integ/descriptor/server_descriptor.py b/test/integ/descriptor/server_descriptor.py
index 0790131..55e6545 100644
--- a/test/integ/descriptor/server_descriptor.py
+++ b/test/integ/descriptor/server_descriptor.py
@@ -16,35 +16,6 @@ import test.runner
 import test.integ.descriptor
 
 class TestServerDescriptor(unittest.TestCase):
-  def test_tutorial(self):
-    """
-    Runs the tutorial example for parsing server descriptors. We use our
-    metrics consensus rather than the cached one so it won't take overly long.
-    """
-    
-    from stem.descriptor.reader import DescriptorReader
-    
-    bw_to_relay = {} # mapping of observed bandwidth to the relay nicknames
-    
-    descriptor_path = test.integ.descriptor.get_resource("example_descriptor")
-    with DescriptorReader([descriptor_path]) as reader:
-      for desc in reader:
-        if desc.exit_policy.is_exiting_allowed():
-          bw_to_relay.setdefault(desc.observed_bandwidth, []).append(desc.nickname)
-    
-    sorted_bw = sorted(bw_to_relay.keys(), reverse = True)
-    
-    # prints the top fifteen relays
-    
-    count = 1
-    for bw_value in sorted_bw:
-      for nickname in bw_to_relay[bw_value]:
-        printed_line = "%i. %s (%i bytes/s)" % (count, nickname, bw_value)
-        count += 1
-        
-        if count > 15:
-          return
-  
   def test_metrics_descriptor(self):
     """
     Parses and checks our results against a server descriptor from metrics.
diff --git a/test/mocking.py b/test/mocking.py
index 1a8aa41..4cfe91a 100644
--- a/test/mocking.py
+++ b/test/mocking.py
@@ -11,6 +11,7 @@ calling :func:`test.mocking.revert_mocking`.
   get_real_function - provides the non-mocked version of a function
   get_all_combinations - provides all combinations of attributes
   support_with - makes object be compatable for use via the 'with' keyword
+  get_object - get an abitrary mock object of any class
   
   Mocking Functions
     no_op           - does nothing
@@ -211,11 +212,15 @@ def return_for_args(args_to_return_value, default = None):
   """
   
   def _return_value(*args):
+    # strip off the 'self' for mock clases
+    if args and 'MockClass' in str(type(args[0])):
+      args = args[1:] if len(args) > 2 else args[1]
+    
     if args in args_to_return_value:
       return args_to_return_value[args]
     elif default is None:
       arg_label = ", ".join([str(v) for v in args])
-      raise ValueError("Unrecognized argument sent for return_for_args(): %s" % arg_label)
+      raise ValueError("Unrecognized argument sent for return_for_args(). Got '%s' but we only recognize '%s'." % (arg_label, ", ".join(args_to_return_value.keys())))
     else:
       return default(args)
   
@@ -384,6 +389,37 @@ def get_all_combinations(attr, include_empty = False):
         seen.add(item)
         yield item
 
+def get_object(object_class, methods = None):
+  """
+  Provides a mock Controller instance. Its methods are mocked with the given
+  replacements, and calling any others will result in an exception.
+  
+  :param class object_class: class that we're making an instance of
+  :param dict methods: mapping of method names to their mocked implementation
+  
+  :returns: stem.control.Controller instance
+  """
+  
+  if methods is None:
+    methods = {}
+  
+  mock_methods = {}
+  
+  for method_name in dir(object_class):
+    if method_name in methods:
+      mock_methods[method_name] = methods[method_name]
+    elif method_name.startswith('__') and method_name.endswith('__'):
+      pass # messing with most private methods makes for a broken mock object
+    else:
+      mock_methods[method_name] = raise_exception(ValueError("Unexpected call of '%s' on a mock object" % method_name))
+  
+  # makes it so our constructor won't need any arguments
+  mock_methods['__init__'] = no_op()
+  
+  mock_class = type('MockClass', (object_class,), mock_methods)
+  
+  return mock_class()
+
 def get_message(content, reformat = True):
   """
   Provides a ControlMessage with content modified to be parsable. This makes
diff --git a/test/unit/tutorial.py b/test/unit/tutorial.py
new file mode 100644
index 0000000..13ef645
--- /dev/null
+++ b/test/unit/tutorial.py
@@ -0,0 +1,80 @@
+"""
+Tests for the examples given in stem's tutorial.
+"""
+
+from __future__ import with_statement
+import unittest
+
+import test.mocking as mocking
+
+class TestTutorial(unittest.TestCase):
+  def tearDown(self):
+    mocking.revert_mocking()
+  
+  def test_the_little_relay_that_could(self):
+    from stem.control import Controller
+    
+    controller = mocking.get_object(Controller, {
+      'authenticate': mocking.no_op(),
+      'close': mocking.no_op(),
+      'get_info': mocking.return_for_args({
+        'traffic/read': '1234',
+        'traffic/written': '5678',
+      }),
+    })
+    
+    controller.authenticate()
+    
+    bytes_read = controller.get_info("traffic/read")
+    bytes_written = controller.get_info("traffic/written")
+    
+    expected_line = "My Tor relay has read 1234 bytes and written 5678."
+    printed_line = "My Tor relay has read %s bytes and written %s." % (bytes_read, bytes_written)
+    self.assertEqual(expected_line, printed_line)
+    
+    controller.close()
+  
+  def test_mirror_mirror_on_the_wall(self):
+    from stem.descriptor.server_descriptor import RelayDescriptor
+    from stem.descriptor.reader import DescriptorReader
+    
+    exit_descriptor = RelayDescriptor(mocking.get_relay_server_descriptor({
+      'router': 'speedyexit 149.255.97.109 9001 0 0'
+    }, content = True).replace('reject *:*', 'accept *:*'))
+    
+    reader_wrapper = mocking.get_object(DescriptorReader, {
+      '__enter__': lambda x: x,
+      '__exit__': mocking.no_op(),
+      '__iter__': mocking.return_value(iter((
+        exit_descriptor,
+        mocking.get_relay_server_descriptor(), # non-exit
+        exit_descriptor,
+        exit_descriptor,
+      )))
+    })
+    
+    bw_to_relay = {} # mapping of observed bandwidth to the relay nicknames
+    
+    with reader_wrapper as reader:
+      for desc in reader:
+        if desc.exit_policy.is_exiting_allowed():
+          bw_to_relay.setdefault(desc.observed_bandwidth, []).append(desc.nickname)
+    
+    sorted_bw = sorted(bw_to_relay.keys(), reverse = True)
+    
+    # prints the top fifteen relays
+    
+    count = 1
+    for bw_value in sorted_bw:
+      for nickname in bw_to_relay[bw_value]:
+        expected_line = "%i. speedyexit (104590 bytes/s)" % count
+        printed_line = "%i. %s (%i bytes/s)" % (count, nickname, bw_value)
+        self.assertEqual(expected_line, printed_line)
+        
+        count += 1
+        
+        if count > 15:
+          return
+    
+    self.assertEqual(4, count)
+





More information about the tor-commits mailing list