Beef up and refactor tests (DRY)
authorGeorge Sakkis <george.sakkis@gmail.com>
Mon, 20 Apr 2015 22:46:51 +0000 (01:46 +0300)
committerGeorge Sakkis <george.sakkis@gmail.com>
Mon, 20 Apr 2015 23:51:25 +0000 (02:51 +0300)
tests/test_cached_property.py
tests/test_cached_property_ttl.py [deleted file]

index cd046633de1ca257929d43775df94a72be1ab0de..725a87ebc4ad5ea2c042235a3de2676ed635aa9b 100644 (file)
 # -*- coding: utf-8 -*-
 
-"""Tests for cached_property and threaded_cached_property"""
-
-from time import sleep
-from threading import Lock, Thread
+import time
 import unittest
+from threading import Lock, Thread
+from freezegun import freeze_time
 
-from cached_property import cached_property, threaded_cached_property
+import cached_property
 
 
-class TestCachedProperty(unittest.TestCase):
-    """Tests for cached_property"""
+def CheckFactory(cached_property_decorator, threadsafe=False):
+    """
+    Create dynamically a Check class whose add_cached method is decorated by
+    the cached_property_decorator.
+    """
 
-    cached_property_factory = cached_property
+    class Check(object):
 
-    def test_cached_property(self):
+        def __init__(self):
+            self.control_total = 0
+            self.cached_total = 0
+            self.lock = Lock()
 
-        class Check(object):
+        @property
+        def add_control(self):
+            self.control_total += 1
+            return self.control_total
 
-            def __init__(self):
-                self.total1 = 0
-                self.total2 = 0
+        @cached_property_decorator
+        def add_cached(self):
+            if threadsafe:
+                time.sleep(1)
+                # Need to guard this since += isn't atomic.
+                with self.lock:
+                    self.cached_total += 1
+            else:
+                self.cached_total += 1
 
-            @property
-            def add_control(self):
-                self.total1 += 1
-                return self.total1
+            return self.cached_total
 
-            @self.cached_property_factory
-            def add_cached(self):
-                self.total2 += 1
-                return self.total2
+        def run_threads(self, num_threads):
+            threads = []
+            for _ in range(num_threads):
+                thread = Thread(target=lambda: self.add_cached)
+                thread.start()
+                threads.append(thread)
+            for thread in threads:
+                thread.join()
 
-        c = Check()
+    return Check
 
-        # The control shows that we can continue to add 1.
-        self.assertEqual(c.add_control, 1)
-        self.assertEqual(c.add_control, 2)
 
-        # The cached version demonstrates how nothing new is added
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.total2, 1)
+class TestCachedProperty(unittest.TestCase):
+    """Tests for cached_property"""
 
-        # It's customary for descriptors to return themselves if accessed
-        # though the class, rather than through an instance.
-        self.assertTrue(isinstance(Check.add_cached, self.cached_property_factory))
+    cached_property_factory = cached_property.cached_property
 
-    def test_reset_cached_property(self):
+    def assert_control(self, check, expected):
+        """
+        Assert that both `add_control` and 'control_total` equal `expected`
+        """
+        self.assertEqual(check.add_control, expected)
+        self.assertEqual(check.control_total, expected)
 
-        class Check(object):
+    def assert_cached(self, check, expected):
+        """
+        Assert that both `add_cached` and 'cached_total` equal `expected`
+        """
+        self.assertEqual(check.add_cached, expected)
+        self.assertEqual(check.cached_total, expected)
 
-            def __init__(self):
-                self.total = 0
+    def test_cached_property(self):
+        Check = CheckFactory(self.cached_property_factory)
+        check = Check()
 
-            @self.cached_property_factory
-            def add_cached(self):
-                self.total += 1
-                return self.total
+        # The control shows that we can continue to add 1
+        self.assert_control(check, 1)
+        self.assert_control(check, 2)
+
+        # The cached version demonstrates how nothing is added after the first
+        self.assert_cached(check, 1)
+        self.assert_cached(check, 1)
 
-        c = Check()
+        # The cache does not expire
+        with freeze_time("9999-01-01"):
+            self.assert_cached(check, 1)
+
+        # Typically descriptors return themselves if accessed though the class
+        # rather than through an instance.
+        self.assertIsInstance(Check.add_cached, self.cached_property_factory)
+
+    def test_reset_cached_property(self):
+        Check = CheckFactory(self.cached_property_factory)
+        check = Check()
 
         # Run standard cache assertion
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.total, 1)
+        self.assert_cached(check, 1)
+        self.assert_cached(check, 1)
 
-        # Reset the cache.
-        del c.add_cached
-        self.assertEqual(c.add_cached, 2)
-        self.assertEqual(c.add_cached, 2)
-        self.assertEqual(c.total, 2)
+        # Clear the cache
+        del check.add_cached
 
-    def test_none_cached_property(self):
+        # Value is cached again after the next access
+        self.assert_cached(check, 2)
+        self.assert_cached(check, 2)
 
+    def test_none_cached_property(self):
         class Check(object):
 
             def __init__(self):
-                self.total = None
+                self.cached_total = None
 
             @self.cached_property_factory
             def add_cached(self):
-                return self.total
-
-        c = Check()
+                return self.cached_total
 
-        # Run standard cache assertion
-        self.assertEqual(c.add_cached, None)
-        self.assertEqual(c.total, None)
+        self.assert_cached(Check(), None)
 
     def test_threads(self):
-        """
-        How well does the standard cached_property implementation work with
-        threads? It doesn't, use threaded_cached_property instead!
-        """
-        num_threads = 10
-        check = self._run_threads(num_threads)
-        # Threads means that caching is bypassed.
+        Check = CheckFactory(self.cached_property_factory, threadsafe=True)
+        check = Check()
+        num_threads = 5
+
+        # cached_property_with_ttl is *not* thread-safe!
+        check.run_threads(num_threads)
         # This assertion hinges on the fact the system executing the test can
         # spawn and start running num_threads threads within the sleep period
         # (defined in the Check class as 1 second). If num_threads were to be
         # massively increased (try 10000), the actual value returned would be
         # between 1 and num_threads, depending on thread scheduling and
         # preemption.
-        self.assertEqual(check.add_cached, num_threads)
-        self.assertEqual(check.total, num_threads)
+        self.assert_cached(check, num_threads)
+        self.assert_cached(check, num_threads)
 
-    def _run_threads(self, num_threads):
-        class Check(object):
+        # The cache does not expire
+        with freeze_time("9999-01-01"):
+            check.run_threads(num_threads)
+            self.assert_cached(check, num_threads)
+            self.assert_cached(check, num_threads)
 
-            def __init__(self):
-                self.total = 0
-                self.lock = Lock()
 
-            @self.cached_property_factory
-            def add_cached(self):
-                sleep(1)
-                # Need to guard this since += isn't atomic.
-                with self.lock:
-                    self.total += 1
-                return self.total
+class TestThreadedCachedProperty(TestCachedProperty):
+    """Tests for threaded_cached_property"""
+
+    cached_property_factory = cached_property.threaded_cached_property
 
-        c = Check()
+    def test_threads(self):
+        Check = CheckFactory(self.cached_property_factory, threadsafe=True)
+        check = Check()
+        num_threads = 5
 
-        threads = []
-        for _ in range(num_threads):
-            thread = Thread(target=lambda: c.add_cached)
-            thread.start()
-            threads.append(thread)
-        for thread in threads:
-            thread.join()
+        # threaded_cached_property_with_ttl is thread-safe
+        check.run_threads(num_threads)
+        self.assert_cached(check, 1)
+        self.assert_cached(check, 1)
 
-        return c
+        # The cache does not expire
+        with freeze_time("9999-01-01"):
+            check.run_threads(num_threads)
+            self.assert_cached(check, 1)
+            self.assert_cached(check, 1)
 
 
-class TestThreadedCachedProperty(TestCachedProperty):
-    """Tests for threaded_cached_property"""
+class TestCachedPropertyWithTTL(TestCachedProperty):
+    """Tests for cached_property_with_ttl"""
 
-    cached_property_factory = threaded_cached_property
+    cached_property_factory = cached_property.cached_property_with_ttl
 
-    def test_threads(self):
-        """How well does this implementation work with threads?"""
-        num_threads = 10
-        check = self._run_threads(num_threads)
-        self.assertEqual(check.add_cached, 1)
-        self.assertEqual(check.total, 1)
+    def test_ttl_expiry(self):
+        Check = CheckFactory(self.cached_property_factory(ttl=100000))
+        check = Check()
+
+        # Run standard cache assertion
+        self.assert_cached(check, 1)
+        self.assert_cached(check, 1)
+
+        # The cache expires in the future
+        with freeze_time("9999-01-01"):
+            self.assert_cached(check, 2)
+            self.assert_cached(check, 2)
+
+        # Things are not reverted when we are back to the present
+        self.assert_cached(check, 2)
+        self.assert_cached(check, 2)
+
+    def test_threads_ttl_expiry(self):
+        Check = CheckFactory(self.cached_property_factory(ttl=100000),
+                             threadsafe=True)
+        check = Check()
+        num_threads = 5
+
+        # Same as in test_threads
+        check.run_threads(num_threads)
+        self.assert_cached(check, num_threads)
+        self.assert_cached(check, num_threads)
+
+        # The cache expires in the future
+        with freeze_time("9999-01-01"):
+            check.run_threads(num_threads)
+            self.assert_cached(check, 2 * num_threads)
+            self.assert_cached(check, 2 * num_threads)
+
+        # Things are not reverted when we are back to the present
+        self.assert_cached(check, 2 * num_threads)
+        self.assert_cached(check, 2 * num_threads)
+
+
+class TestThreadedCachedPropertyWithTTL(TestThreadedCachedProperty,
+                                        TestCachedPropertyWithTTL):
+    """Tests for threaded_cached_property_with_ttl"""
+
+    cached_property_factory = cached_property.threaded_cached_property_with_ttl
+
+    def test_threads_ttl_expiry(self):
+        Check = CheckFactory(self.cached_property_factory(ttl=100000),
+                             threadsafe=True)
+        check = Check()
+        num_threads = 5
+
+        # Same as in test_threads
+        check.run_threads(num_threads)
+        self.assert_cached(check, 1)
+        self.assert_cached(check, 1)
+
+        # The cache expires in the future
+        with freeze_time("9999-01-01"):
+            check.run_threads(num_threads)
+            self.assert_cached(check, 2)
+            self.assert_cached(check, 2)
+
+        # Things are not reverted when we are back to the present
+        self.assert_cached(check, 2)
+        self.assert_cached(check, 2)
diff --git a/tests/test_cached_property_ttl.py b/tests/test_cached_property_ttl.py
deleted file mode 100644 (file)
index 26f637e..0000000
+++ /dev/null
@@ -1,304 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""Tests for cached_property_with_ttl and threaded_cache_property_with_ttl"""
-
-import unittest
-from freezegun import freeze_time
-from time import sleep
-from threading import Lock, Thread
-
-from cached_property import (
-    cached_property_with_ttl,
-    threaded_cached_property_with_ttl
-)
-
-
-class TestCachedPropertyWithTTL(unittest.TestCase):
-
-    def test_cached_property(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total1 = 0
-                self.total2 = 0
-
-            @property
-            def add_control(self):
-                self.total1 += 1
-                return self.total1
-
-            @cached_property_with_ttl
-            def add_cached(self):
-                self.total2 += 1
-                return self.total2
-
-        c = Check()
-
-        # The control shows that we can continue to add 1.
-        self.assertEqual(c.add_control, 1)
-        self.assertEqual(c.add_control, 2)
-
-        # The cached version demonstrates how nothing new is added
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.add_cached, 1)
-
-        # Cannot expire the cache.
-        with freeze_time("9999-01-01"):
-            self.assertEqual(c.add_cached, 1)
-
-        # It's customary for descriptors to return themselves if accessed
-        # though the class, rather than through an instance.
-        self.assertTrue(isinstance(Check.add_cached, cached_property_with_ttl))
-
-    def test_reset_cached_property(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = 0
-
-            @cached_property_with_ttl
-            def add_cached(self):
-                self.total += 1
-                return self.total
-
-        c = Check()
-
-        # Resetting the cache before it is set is a no-op
-        del c.add_cached
-
-        # Run standard cache assertion
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.add_cached, 1)
-
-        # Reset the cache.
-        del c.add_cached
-        self.assertEqual(c.add_cached, 2)
-        self.assertEqual(c.add_cached, 2)
-
-    def test_none_cached_property(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = None
-
-            @cached_property_with_ttl
-            def add_cached(self):
-                return self.total
-
-        c = Check()
-
-        # Run standard cache assertion
-        self.assertEqual(c.add_cached, None)
-
-    def test_threads(self):
-        """ How well does the standard cached_property implementation work with threads?
-            Short answer: It doesn't! Use threaded_cached_property instead!
-        """  # noqa
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = 0
-                self.lock = Lock()
-
-            @cached_property_with_ttl
-            def add_cached(self):
-                sleep(1)
-                # Need to guard this since += isn't atomic.
-                with self.lock:
-                    self.total += 1
-                return self.total
-
-        c = Check()
-        threads = []
-        num_threads = 10
-        for x in range(num_threads):
-            thread = Thread(target=lambda: c.add_cached)
-            thread.start()
-            threads.append(thread)
-
-        for thread in threads:
-            thread.join()
-
-        # Threads means that caching is bypassed.
-        self.assertNotEqual(c.add_cached, 1)
-
-        # This assertion hinges on the fact the system executing the test can
-        # spawn and start running num_threads threads within the sleep period
-        # (defined in the Check class as 1 second). If num_threads were to be
-        # massively increased (try 10000), the actual value returned would be
-        # between 1 and num_threads, depending on thread scheduling and
-        # preemption.
-        self.assertEqual(c.add_cached, num_threads)
-
-    def test_ttl_expiry(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = 0
-
-            @cached_property_with_ttl(ttl=100000)
-            def add_cached(self):
-                self.total += 1
-                return self.total
-
-        c = Check()
-
-        # Run standard cache assertion
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.add_cached, 1)
-
-        # Expire the cache.
-        with freeze_time("9999-01-01"):
-            self.assertEqual(c.add_cached, 2)
-        self.assertEqual(c.add_cached, 2)
-
-
-class TestThreadedCachedPropertyWithTTL(unittest.TestCase):
-
-    def test_cached_property(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total1 = 0
-                self.total2 = 0
-
-            @property
-            def add_control(self):
-                self.total1 += 1
-                return self.total1
-
-            @threaded_cached_property_with_ttl
-            def add_cached(self):
-                self.total2 += 1
-                return self.total2
-
-        c = Check()
-
-        # The control shows that we can continue to add 1.
-        self.assertEqual(c.add_control, 1)
-        self.assertEqual(c.add_control, 2)
-
-        # The cached version demonstrates how nothing new is added
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.add_cached, 1)
-
-        # Cannot expire the cache.
-        with freeze_time("9999-01-01"):
-            self.assertEqual(c.add_cached, 1)
-
-        # It's customary for descriptors to return themselves if accessed
-        # though the class, rather than through an instance.
-        self.assertTrue(isinstance(Check.add_cached, threaded_cached_property_with_ttl))
-
-    def test_reset_cached_property(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = 0
-
-            @threaded_cached_property_with_ttl
-            def add_cached(self):
-                self.total += 1
-                return self.total
-
-        c = Check()
-
-        # Resetting the cache before it is set is a no-op
-        del c.add_cached
-
-        # Run standard cache assertion
-        self.assertEqual(c.add_cached, 1)
-        self.assertEqual(c.add_cached, 1)
-
-        # Reset the cache.
-        del c.add_cached
-        self.assertEqual(c.add_cached, 2)
-        self.assertEqual(c.add_cached, 2)
-
-    def test_none_cached_property(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = None
-
-            @threaded_cached_property_with_ttl
-            def add_cached(self):
-                return self.total
-
-        c = Check()
-
-        # Run standard cache assertion
-        self.assertEqual(c.add_cached, None)
-
-    def test_threads(self):
-        """ How well does this implementation work with threads?"""
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = 0
-                self.lock = Lock()
-
-            @threaded_cached_property_with_ttl
-            def add_cached(self):
-                sleep(1)
-                # Need to guard this since += isn't atomic.
-                with self.lock:
-                    self.total += 1
-                return self.total
-
-        c = Check()
-        threads = []
-        for x in range(10):
-            thread = Thread(target=lambda: c.add_cached)
-            thread.start()
-            threads.append(thread)
-
-        for thread in threads:
-            thread.join()
-
-        self.assertEqual(c.add_cached, 1)
-
-    def test_ttl_expiry(self):
-
-        class Check(object):
-
-            def __init__(self):
-                self.total = 0
-                self.lock = Lock()
-
-            @threaded_cached_property_with_ttl(ttl=100000)
-            def add_cached(self):
-                sleep(1)
-                # Need to guard this since += isn't atomic.
-                with self.lock:
-                    self.total += 1
-                return self.total
-
-        def run_threads(check, num_threads=10):
-            threads = []
-            for _ in range(num_threads):
-                thread = Thread(target=lambda: check.add_cached)
-                thread.start()
-                threads.append(thread)
-            for thread in threads:
-                thread.join()
-
-        c = Check()
-        run_threads(c)
-        self.assertEqual(c.add_cached, 1)
-
-        # Expire the cache.
-        with freeze_time("9999-01-01"):
-            run_threads(c)
-            self.assertEqual(c.add_cached, 2)
-
-        self.assertEqual(c.add_cached, 2)