Commits

Anonymous committed 49f9c7f

Tests refactoring #5.

  • Participants
  • Parent commits a84b930

Comments (0)

Files changed (3)

         else:
             self._runs_failure.append(run)
 
+    def _save_main_index(self, main_index):
+        execute = lambda query, *args: self.cursor.execute(query, args)
+        fetchone = self.cursor.fetchone
+
+        # Insert main_index into main_indices table.
+        execute('''INSERT INTO main_indices
+                   (absolute, relative)
+                   VALUES (%d, %d)''',
+                main_index.absolute,
+                main_index.relative)
+        execute('SELECT last_value FROM main_indices_id_seq')
+        main_index_id = fetchone()[0]
+
+        # Insert each of its subindices.
+        for subindex in main_index.subindices:
+            execute('''INSERT INTO subindices
+                       VALUES (%d, %s, %d, %s)''',
+                    main_index_id,
+                    subindex.name,
+                    subindex.value,
+                    subindex.details)
+
+        return main_index_id
+
+    def _release_exists(self, name, version):
+        execute = lambda query, *args: self.cursor.execute(query, args)
+        fetchall = self.cursor.fetchall
+
+        execute('''SELECT *
+                   FROM releases
+                   WHERE name = %s AND version = %s''',
+                name,
+                version)
+
+        if fetchall():
+            return True
+        return False
+
+    def _remove_indices_for_release(self, name, version):
+        execute = lambda query, *args: self.cursor.execute(query, args)
+        fetchone = self.cursor.fetchone
+
+        execute('''SELECT installability_id, documentation_id, code_kwalitee_id
+                   FROM releases
+                   WHERE name = %s AND version = %s''',
+                name,
+                version)
+
+        main_index_ids = fetchone()
+        for index in main_index_ids:
+            execute('''DELETE FROM subindices
+                       WHERE main_index_id = %d''',
+                    index)
+            execute('''DELETE FROM releases
+                       WHERE name = %s AND version = %s''',
+                    name,
+                    version)
+            execute('''DELETE FROM main_indices
+                       WHERE id = %d''',
+                    index)
+
+    def _insert_score_for_release(self, name, version, installability_id, documentation_id, code_kwalitee_id):
+        execute = lambda query, *args: self.cursor.execute(query, args)
+
+        execute('''INSERT INTO releases
+                   VALUES (%s, %s, %d, %d, %d)''',
+                name,
+                version,
+                installability_id,
+                documentation_id,
+                code_kwalitee_id)
+
     def save_score(self, name, version, score):
         """Save Cheesecake score into database.
         """
         if self.closed:
             raise ValueError("Operation on closed store.")
 
-        execute = lambda query, *args: self.cursor.execute(query, args)
-        fetchone = self.cursor.fetchone
-        fetchall = self.cursor.fetchall
-
-        def save_main_index(main_index):
-            # Insert main_index into main_indices table.
-            execute('''INSERT INTO main_indices
-                       (absolute, relative)
-                       VALUES (%d, %d)''',
-                    main_index.absolute,
-                    main_index.relative)
-            execute('SELECT last_value FROM main_indices_id_seq')
-            main_index_id = fetchone()[0]
-
-            # Insert each of its subindices.
-            for subindex in main_index.subindices:
-                execute('''INSERT INTO subindices
-                           VALUES (%d, %s, %d, %s)''',
-                        main_index_id,
-                        subindex.name,
-                        subindex.value,
-                        subindex.details)
-
-            return main_index_id
-
-        def release_exists(name, version):
-            execute('''SELECT *
-                       FROM releases
-                       WHERE name = %s AND version = %s''',
-                    name,
-                    version)
-
-            if fetchall():
-                return True
-            return False
-
-        def remove_indices_for_release(name, version):
-            execute('''SELECT installability_id, documentation_id, code_kwalitee_id
-                       FROM releases
-                       WHERE name = %s AND version = %s''',
-                    name,
-                    version)
-
-            main_index_ids = fetchone()
-            for index in main_index_ids:
-                execute('''DELETE FROM subindices
-                           WHERE main_index_id = %d''',
-                        index)
-                execute('''DELETE FROM releases
-                           WHERE name = %s AND version = %s''',
-                        name,
-                        version)
-                execute('''DELETE FROM main_indices
-                           WHERE id = %d''',
-                        index)
-
-        def insert_score_for_release(name, version, installability_id, documentation_id, code_kwalitee_id):
-            execute('''INSERT INTO releases
-                       VALUES (%s, %s, %d, %d, %d)''',
-                    name,
-                    version,
-                    installability_id,
-                    documentation_id,
-                    code_kwalitee_id)
-
-        installability_id = save_main_index(score.installability)
-        documentation_id = save_main_index(score.documentation)
-        code_kwalitee_id = save_main_index(score.code_kwalitee)
+        installability_id = self._save_main_index(score.installability)
+        documentation_id = self._save_main_index(score.documentation)
+        code_kwalitee_id = self._save_main_index(score.code_kwalitee)
 
         # If score for a release already exist, remove it first.
-        if release_exists(name, version):
-            remove_indices_for_release(name, version)
+        if self._release_exists(name, version):
+            self._remove_indices_for_release(name, version)
 
-        insert_score_for_release(name, version, installability_id, documentation_id, code_kwalitee_id)
+        self._insert_score_for_release(name, version, installability_id, documentation_id, code_kwalitee_id)
 
         # Commit changes and update self.score dictionary.
         self.db.commit()

File tests/mock.py

      
     def _setupSubclassMethodInterceptors(self):
         methods = inspect.getmembers(self.__class__,inspect.isroutine)
-        baseMethods = dict(inspect.getmembers(Mock, inspect.ismethod))
+        baseMethods = dict(inspect.getmembers(self.__class__, inspect.ismethod))
         for m in methods:
             name = m[0]
             # Don't record calls to methods of Mock base class.
     def mockAddReturnValues(self, **methodReturnValues ):
         self.mockReturnValues.update(methodReturnValues)
         
-    def mockSetExpectation(self, name, testFn, after=0, until=0, at=None):
-        if after == until == 0 and at is not None:
-            after = at - 1
-            until = at + 1
+    def mockSetExpectation(self, name, testFn, after=0, until=0):
         self.mockExpectations.setdefault(name, []).append((testFn,after,until))
 
     def _checkInterfaceCall(self, name, callParams, callKwParams):

File tests/test_store.py

 import unittest
 
 from _test_case_with_spec import TestCaseWithSpec
-from mock import Mock
+from mock import Mock, ReturnValues
 import _path_setup
 
 import store
 ################################################################################
 ## Test helpers.
 ################################################################################
+
 def pluralize(word, count):
     """Pluralize word if count > 1.
 
     """
     return lambda *a: value
 
+class OF_TYPE(object):
+    def __init__(self, expected_type):
+        self.type = expected_type
+
 def args_are(*values):
     """Return a predicate that checks if its arguments are equal to `values`.
 
     True
     >>> predicate(1, [2], (3,))
     False
+
+    Special OF_TYPE values can be used to mark expected argument type:
+    >>> predicate = args_are(42, OF_TYPE(str))
+    >>> predicate(42, 'hello')
+    True
+    >>> predicate(42, 123)
+    False
+    >>> predicate(42)
+    False
     """
-    return lambda *a: a == values
+    def predicate(*args):
+        if len(args) != len(values): return False
+        for got, expected in zip(args, values):
+            if isinstance(expected, OF_TYPE):
+                if not isinstance(got, expected.type): return False
+            else:
+                if got != expected: return False
+        return True
 
-def expectation_args_are(*values):
-    """Return a predicate for use with mockSetExpectation.
-
-    >>> mock = Mock()
-    >>> mock.mockSetExpectation('call', expectation_args_are(1, 2, 3))
-    >>> mock.call(1, 2, 3) # ok
-    >>> mock.call('a', 'b', 'c')
-    Traceback (most recent call last):
-      ...
-    AssertionError: Expectation failed: call('a', 'b', 'c')
-    """
-    return lambda mock, call, count: args_are(*values)(*call.params)
+    return predicate
 
 def check_calls_count(mock, name, exactly=None, min=None, max=None):
     """Assert that given method was called given number of times
     if max is not None:
         assert calls_number <= max, "Called %s more than %d %s" % (name, max, pluralize('time', max))
 
-class StoreMock(Store, Mock):
-    """Behave like a Store with few exceptions:
+def check_calls(mock, expectations):
+    """Check that methods were called in given order and with given arguments.
 
-    * don't call any of initialization methods (helps in testing each one
-      of them separately), only set attributes
-    * inherit from Mock, so we can set expectations on store object
+    >>> m = Mock()
+    >>> m.foo()
+    >>> m.bar(1, 2)
+    >>> m.baz('whatever')
+    >>> check_calls(m, ['foo', 'bar', 'baz']) # ok
+    >>> check_calls(m, ['foo', ('bar', args_are(1, 2)), ('baz', args_are(OF_TYPE(str)))]) # ok
+    >>> check_calls(m, ['foo'])
+    Traceback (most recent call last):
+      ...
+    AssertionError: Unexpected call to bar(1, 2)
+    >>> check_calls(m, [('foo', args_are(1)), 'bar', 'baz'])
+    Traceback (most recent call last):
+      ...
+    AssertionError: Expectation failed for method 'foo': got foo()
     """
-    def __init__(self):
-        Mock.__init__(self)
+    calls = mock.mockGetAllCalls()
 
-        self.directory = '/log_directory'
-        self.timestamp_file = 'timestamp_file'
+    for i in xrange(max(len(expectations), len(calls))):
+        assert i < len(expectations), "Unexpected call to %s" % calls[i]
+        assert i < len(calls), "Expected call to '%s', got nothing instead" % \
+               (isinstance(expectations[i], str) and expectations[i] or expectations[i][0])
 
-        self.closed = False
+        expect, call = expectations[i], calls[i]
 
-        self._runs_by_release = {}
-        self._runs_by_number = {}
-        self._runs_failure = []
-        self._runs_success = []
+        if isinstance(expect, str):
+            method = expect
+            predicate = returning(True)
+        else:
+            method, predicate = expect
 
-        self.releases_to_score = []
-        self.score = {}
-        self.timestamp = 0
+        assert call.name == method, "Expected call to '%s', got %s" % (method, call)
+        assert predicate(*call.params, **call.kwparams), "Expectation failed for method '%s': got %s" % (method, call)
 
-def make_postgres_mock(execute=None,
-                       fetchall=None,
-                       fetchone=None,
-                       commit=None,
-                       close=None):
-    cursor_mock = Mock({'execute': execute,
-                        'fetchall': fetchall,
-                        'fetchone': fetchone})
+class Pretender(object):
+    def as(self, klass, method):
+        """Pretend we're object of given `klass` which can receive a `method`.
 
-    connection_mock = Mock({'commit': commit,
-                            'cursor': cursor_mock,
-                            'close': close})
+        >>> class A(object):
+        ...    def inc(self, n):
+        ...        return n + 1
+        >>> p = Pretender()
+        >>> p.as(A, 'inc')(5)
+        6
+        """
+        return lambda *args, **kwds: klass.__dict__[method](self, *args, **kwds)
 
-    mock = Mock({'connect': connection_mock})
+    def copy_method(self, klass, method):
+        """Copy a method from `klass` into this object.
 
-    mock.cursor_mock = cursor_mock
-    mock.connection_mock = connection_mock
+        >>> class A(object):
+        ...     def inc(self, n):
+        ...         return n + 1
+        >>> p = Pretender()
+        >>> p.inc(11)
+        Traceback (most recent call last):
+          ...
+        AttributeError: 'Pretender' object has no attribute 'inc'
+        >>> p.copy_method(A, 'inc')
+        >>> p.inc(11)
+        12
+        """
+        self.__dict__[method] = self.as(klass, method)
 
-    return (mock, connection_mock, cursor_mock)
+    def copy_methods(self, klass, *methods):
+        """Copy methods from `klass` into this object.
+        """
+        for method in methods:
+            self.copy_method(klass, method)
+
+class PretenderMock(Mock, Pretender): pass
+
+def make_store_mock(mock_values={}, cursor={}, db={}):
+    mock = PretenderMock(mock_values, Store)
+
+    mock.directory = '/log_directory'
+    mock.timestamp_file = 'timestamp_file'
+    mock.logname_regex = Store.logname_regex
+
+    mock.closed = False
+
+    mock._runs_by_release = {}
+    mock._runs_by_number = {}
+    mock._runs_failure = []
+    mock._runs_success = []
+
+    mock.releases_to_score = []
+    mock.score = {}
+    mock.timestamp = 0
+
+    mock.cursor = Mock(cursor)
+    mock.db = Mock(db)
+
+    return mock
 
 ################################################################################
 ## Data.
 ]
 
 ################################################################################
-## Test classes.
+## Unit tests.
 ################################################################################
 
 class TestStoreInit(TestCaseWithSpec):
         check_subindices(score.documentation, sub_scores_documentation)
         check_subindices(score.code_kwalitee, sub_scores_code_kwalitee)
 
+class TestCheesecakeScore(TestCaseWithSpec):
+    context = "Class CheesecakeScore"
+
+    def test_can_serialize_itself(self):
+        MainIndex = CheesecakeScore.MainIndex
+        SubIndex = CheesecakeScore.SubIndex
+
+        expected = {
+            'INSTALLABILITY': (40,
+                               10,
+                               [('unpack', 25, 'package unpacked successfully'),
+                               ('unpack_dir', 15, 'unpack directory is cheesecake-0.6 as expected')]),
+            'DOCUMENTATION': (214,
+                              20,
+                              [('required_files', 180, '6 files and 2 required directories found'),
+                              ('docstrings', 34, 'found 109/328=33.23% objects with docstrings'),
+                              ('formatted_docstrings', 0, 'found 20/328=6.10% objects with formatted docstrings')]),
+            'CODE_KWALITEE': (65,
+                              30,
+                              [('pylint', 35, 'pylint score was 7.00 out of 10'),
+                              ('unit_tested', 30, 'has unit tests')])
+        }
+
+        installability_sub = [
+            SubIndex('unpack', 25, 'package unpacked successfully'),
+            SubIndex('unpack_dir''', 15, 'unpack directory is cheesecake-0.6 as expected'),
+        ]
+        documentation_sub = [
+            SubIndex('required_files', 180, '6 files and 2 required directories found'),
+            SubIndex('docstrings', 34, 'found 109/328=33.23% objects with docstrings'),
+            SubIndex('formatted_docstrings', 0, 'found 20/328=6.10% objects with formatted docstrings'),
+        ]
+        code_kwalitee_sub = [
+            SubIndex('pylint', 35, 'pylint score was 7.00 out of 10'),
+            SubIndex('unit_tested', 30, 'has unit tests'),
+        ]
+
+        installability = MainIndex(40, 10, installability_sub)
+        documentation = MainIndex(214, 20, documentation_sub)
+        code_kwalitee = MainIndex(65, 30, code_kwalitee_sub)
+
+        score = CheesecakeScoreFromValues(installability, documentation, code_kwalitee)
+
+        serialized = score.serialize()
+
+        assert serialized == expected
+
 class TestReadCheesecakeRuns(TestCaseWithSpec):
     context = "Method Store._read_cheesecake_runs()"
 
     def test_doesnt_call_save_run_for_empty_log_directory(self):
         os.listdir = returning([])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        store_mock = make_store_mock()
+        store_mock.as(Store, '_read_cheesecake_runs')()
 
-        check_calls_count(store, 'save_run', exactly=0)
+        check_calls_count(store_mock, 'save_run', exactly=0)
 
     def test_calls_save_run_four_times_for_four_lognames_in_log_directory(self):
         os.listdir = returning(['1-twill==0.8.5-success-42.log',
                                 '3-PIL==1.1.5-failure-42.log',
                                 '4-setuptools==0.6c1-success-42.log'])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        store_mock = make_store_mock()
+        store_mock.as(Store, '_read_cheesecake_runs')()
 
-        check_calls_count(store, 'save_run', exactly=4)
+        check_calls_count(store_mock, 'save_run', exactly=4)
 
     def test_calls_save_run_three_times_for_three_lognames_with_successive_numbers(self):
         os.listdir = returning(['12-rope==0.2-success-42.log',
                                 '13-ctypes==1.0.0-failure-42.log',
                                 '101-chimera==0.4.4-failure-42.log'])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        store_mock = make_store_mock()
+        store_mock.as(Store, '_read_cheesecake_runs')()
 
-        check_calls_count(store, 'save_run', exactly=3)
+        check_calls_count(store_mock, 'save_run', exactly=3)
 
     def test_ignores_files_that_doesnt_match_cheesecake_logfile_pattern(self):
         os.listdir = returning(['foobar',
                                 'some random file with spaces',
                                 'different.log'])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        store_mock = make_store_mock()
+        store_mock.as(Store, '_read_cheesecake_runs')()
 
-        check_calls_count(store, 'save_run', exactly=1)
+        check_calls_count(store_mock, 'save_run', exactly=1)
 
     def test_calls_save_run_with_appropriate_cheesecake_run_object(self):
         os.listdir = returning(['1-twill==0.8.5-success-42.log'])
 
-        def check_run(mock, call, count):
-            run = call.getParam(0)
+        def check_run(run):
             return isinstance(run, CheesecakeRun) \
                    and run.number == 1 \
                    and run.package == 'twill' \
                    and run.result == 'success' \
                    and run.execution_time == 42
 
-        store = StoreMock()
-        store.mockSetExpectation('save_run', check_run)
-        store._read_cheesecake_runs()
+        store_mock = make_store_mock()
+
+        store_mock.as(Store, '_read_cheesecake_runs')()
+
+        check_calls(store_mock, [('save_run', check_run)])
 
 class TestRunsCount(TestCaseWithSpec):
     context = "Method Store._get_runs_count()"
 
     def setUp(self):
         self.original_os_listdir = os.listdir
+        self.store_mock = make_store_mock()
+        self.store_mock.copy_methods(Store,
+                                     '_read_cheesecake_runs',
+                                     '_get_runs_count',
+                                     'save_run')
 
     def tearDown(self):
         os.listdir = self.original_os_listdir
     def test_returns_one_for_no_runs(self):
         os.listdir = returning([])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        self.store_mock._read_cheesecake_runs()
 
-        self.assertEqual(store.runs_count, 1)
+        self.assertEqual(self.store_mock._get_runs_count(), 1)
 
     def test_returns_five_for_four_successive_runs_1_2_3_and_4(self):
         os.listdir = returning(['1-twill==0.8.5-success-42.log',
                                 '3-PIL==1.1.5-failure-42.log',
                                 '4-setuptools==0.6c1-success-42.log'])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        self.store_mock._read_cheesecake_runs()
 
-        self.assertEqual(store.runs_count, 5)
+        self.assertEqual(self.store_mock._get_runs_count(), 5)
 
     def test_returns_102_for_three_runs_and_last_with_number_101(self):
         os.listdir = returning(['12-rope==0.2-success-42.log',
                                 '13-ctypes==1.0.0-failure-42.log',
                                 '101-chimera==0.4.4-failure-42.log'])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        self.store_mock._read_cheesecake_runs()
 
-        self.assertEqual(store.runs_count, 102)
+        self.assertEqual(self.store_mock._get_runs_count(), 102)
 
 class TestGetFailures(TestCaseWithSpec):
     context = "Method Store._get_failures()"
 
+    def setUp(self):
+        self.original_os_listdir = os.listdir
+        self.store_mock = make_store_mock()
+        self.store_mock.copy_methods(Store,
+                                     '_map_to_lognames',
+                                     '_read_cheesecake_runs',
+                                     '_get_failures',
+                                     'save_run')
+
+    def tearDown(self):
+        os.listdir = self.original_os_listdir
+
     def test_returns_empty_list_for_successive_lognames(self):
         os.listdir = returning(['12-rope==0.2-success-42.log',
                                 '1-twill==0.8.5-success-42.log'])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        self.store_mock._read_cheesecake_runs()
 
-        self.assertEqual(store.failures, [])
+        self.assertEqual(self.store_mock._get_failures(), [])
 
     def test_returns_only_logs_with_failure_status(self):
         os.listdir = returning(['1-twill==0.8.5-success-42.log',
                                 '3-PIL==1.1.5-failure-42.log',
                                 '4-setuptools==0.6c1-success-42.log'])
 
-        store = StoreMock()
-        store._read_cheesecake_runs()
+        self.store_mock._read_cheesecake_runs()
 
-        self.assertEqual(set(store.failures),
+        self.assertEqual(set(self.store_mock._get_failures()),
                          set(['2-PIL==1.1.5-failure-42.log',
                               '3-PIL==1.1.5-failure-42.log']))
 
 class TestReleasesToScore(TestCaseWithSpec):
     context = "Attribute Store.releases_to_score"
 
-    def setUp(self):
-        self.original_store_psycopg = store.psycopg
-
-    def tearDown(self):
-        store.psycopg = self.original_store_psycopg
-
     def test_is_empty_when_database_table_is_empty(self):
-        store.psycopg = make_postgres_mock(fetchall=[])[0]
-
-        store_mock = StoreMock()
-        store_mock._init_database()
-        store_mock._read_releases_to_score()
+        store_mock = make_store_mock(cursor={'fetchall': []})
+        store_mock.as(Store, '_read_releases_to_score')()
 
         self.assertEqual(store_mock.releases_to_score, [])
 
     def test_is_preserved_in_a_database(self):
-        psycopg_mock, connection_mock, cursor_mock = make_postgres_mock()
-        store.psycopg = psycopg_mock
-
-        store_mock = StoreMock()
-        store_mock._init_database()
+        store_mock = make_store_mock()
         store_mock.releases_to_score = [
             ('cheesecake', '0.6'),
             ('cheesecake', '0.7'),
             ('twill', '0.8.5'),
         ]
 
-        # Table should be cleaned first...
-        cursor_mock.mockSetExpectation('execute',
-                                       expectation_args_are('DELETE FROM releases_to_score', ()),
-                                       at=1)
-        # ...then filled with entries.
-        cursor_mock.mockSetExpectation('execute',
-                                       expectation_args_are('INSERT INTO releases_to_score VALUES (%s, %s)', ('cheesecake', '0.6')),
-                                       at=2)
-        cursor_mock.mockSetExpectation('execute',
-                                       expectation_args_are('INSERT INTO releases_to_score VALUES (%s, %s)', ('cheesecake', '0.7')),
-                                       at=3)
-        cursor_mock.mockSetExpectation('execute',
-                                       expectation_args_are('INSERT INTO releases_to_score VALUES (%s, %s)', ('twill', '0.8.5')),
-                                       at=4)
+        store_mock.as(Store, '_save_releases_to_score')()
 
-        # Expectations are checked as we go.
-        store_mock._save_releases_to_score()
+        check_calls(store_mock.cursor, [
+            # Table should be cleaned first...
+            ('execute', args_are('DELETE FROM releases_to_score', ())),
+            # ...then filled with entries.
+            ('execute', args_are('INSERT INTO releases_to_score VALUES (%s, %s)', ('cheesecake', '0.6'))),
+            ('execute', args_are('INSERT INTO releases_to_score VALUES (%s, %s)', ('cheesecake', '0.7'))),
+            ('execute', args_are('INSERT INTO releases_to_score VALUES (%s, %s)', ('twill', '0.8.5')))])
 
     def test_can_be_initialized_from_database(self):
         list_of_releases = [('cheesecake', '0.6'),
                             ('cheesecake', '0.7'),
                             ('twill', '0.8.5')]
 
-        psycopg_mock, connection_mock, cursor_mock = make_postgres_mock(fetchall=list_of_releases[:])
-        store.psycopg = psycopg_mock
+        store_mock = make_store_mock(cursor={'fetchall': list_of_releases[:]})
 
-        store_mock = StoreMock()
-        store_mock._init_database()
+        # Call method we're interested in.
+        store_mock.as(Store, '_read_releases_to_score')()
 
-        cursor_mock.mockSetExpectation('execute',
-                                       expectation_args_are('SELECT * FROM releases_to_score', ()),
-                                       at=1)
+        check_calls(store_mock.cursor, [
+            ('execute', args_are('SELECT * FROM releases_to_score', ())),
+            'fetchall'])
+        self.assertEqual(store_mock.releases_to_score, list_of_releases)
 
-        store_mock._read_releases_to_score()
 
-        self.assertEqual(store_mock.releases_to_score, list_of_releases)
+class TestSaveScore(TestCaseWithSpec):
+    context = "Method Store.save_score()"
+
+    def test_saves_main_indexes_with_all_subindexes(self):
+        store_mock = make_store_mock({'_save_main_index': ReturnValues(1,2,3),
+                                      '_release_exists': False})
+
+        durus_score = CheesecakeScore(cheesecake_run_output)
+
+        # Pretend our mock is a Store class object.
+        store_mock.as(Store, 'save_score')('Durus', '3.4.1', durus_score)
+
+        check_calls(store_mock, [
+            # Should save main indexes first...
+            ('_save_main_index', args_are(durus_score.installability)),
+            ('_save_main_index', args_are(durus_score.documentation)),
+            ('_save_main_index', args_are(durus_score.code_kwalitee)),
+            # ...then check if release score already exists (it doesn't)...
+            ('_release_exists', args_are('Durus', '3.4.1')),
+            # ...and finally save new release score.
+            ('_insert_score_for_release', args_are('Durus', '3.4.1', 1, 2, 3)),
+            # Should use _add_score to keep Store up-to-date.
+            ('_add_score', args_are('Durus', '3.4.1', durus_score))])
+
+    def test_will_replace_old_score_with_new_one(self):
+        store_mock = make_store_mock({'_save_main_index': ReturnValues(1,2,3),
+                                      '_release_exists': True})
+
+        durus_score = CheesecakeScore(cheesecake_run_output)
+
+        store_mock.as(Store, 'save_score')('Durus', '3.4.1', durus_score)
+
+        check_calls(store_mock, [
+            # Should save main indexes first...
+            ('_save_main_index', args_are(durus_score.installability)),
+            ('_save_main_index', args_are(durus_score.documentation)),
+            ('_save_main_index', args_are(durus_score.code_kwalitee)),
+            # ...then check if release score already exists (it does)...
+            ('_release_exists', args_are('Durus', '3.4.1')),
+            # ...then remove an old score...
+            ('_remove_indices_for_release', args_are('Durus', '3.4.1')),
+            # ...and finally save new release score.
+            ('_insert_score_for_release', args_are('Durus', '3.4.1', 1, 2, 3)),
+            # Should use _add_score to keep Store up-to-date.
+            ('_add_score', args_are('Durus', '3.4.1', durus_score))])
+
+class TestNumberOfFailures(TestCaseWithSpec):
+    context = "Method Store.number_of_failures()"
+
+    def setUp(self):
+        runs_list = [
+            CheesecakeRun(1, 'cheesecake', '0.6', 'failure'),
+            CheesecakeRun(2, 'Durus', '3.4.1', 'success'),
+            CheesecakeRun(3, 'cheesecake', '0.6', 'failure'),
+            CheesecakeRun(4, 'cheesecake', '0.6', 'failure'),
+        ]
+
+        self.store_mock = make_store_mock()
+        self.store_mock.copy_methods(Store,
+                                'save_run',
+                                'number_of_failures')
+
+        for run in runs_list:
+            self.store_mock.save_run(run)
+
+    def test_counts_number_of_failures_for_a_release(self):
+        assert 3 == self.store_mock.number_of_failures('cheesecake', '0.6')
+
+    def test_rises_by_one_on_new_failure_of_this_release(self):
+        self.store_mock.save_run(CheesecakeRun(5, 'cheesecake', '0.6', 'failure'))
+
+        assert 4 == self.store_mock.number_of_failures('cheesecake', '0.6')
+
+    def test_doesnt_mix_up_different_packages_failures(self):
+        self.store_mock.save_run(CheesecakeRun(6, 'twill', '0.8.5', 'success'))
+
+        assert 3 == self.store_mock.number_of_failures('cheesecake', '0.6')
+
+    def test_doesnt_mix_up_different_versions_of_the_same_package(self):
+        self.store_mock.save_run(CheesecakeRun(7, 'cheesecake', '0.7', 'failure'))
+
+        assert 3 == self.store_mock.number_of_failures('cheesecake', '0.6')
+
+    def test_ignores_success_runs_for_other_versions(self):
+        self.store_mock.save_run(CheesecakeRun(7, 'cheesecake', '0.7', 'success'))
+
+        assert 3 == self.store_mock.number_of_failures('cheesecake', '0.6')
+
+    def test_is_zero_when_last_run_was_a_success(self):
+        self.store_mock.save_run(CheesecakeRun(7, 'cheesecake', '0.6', 'success'))
+
+        assert 0 == self.store_mock.number_of_failures('cheesecake', '0.6')
+
+################################################################################
+## Functional tests.
+################################################################################
 
 class TestCaseWithTestDatbase(unittest.TestCase):
     postgres_config = 'dbname=cheesecake_test'
         store = Store(postgres_config=self.postgres_config)
         assert store.score[('Durus', '3.4.1')].documentation.absolute == 184
 
-
-class NumberOfFailuresTest(unittest.TestCase):
-    def test_number_of_failures(self):
-        runs_list = [
-            CheesecakeRun(1, 'cheesecake', '0.6', 'failure'),
-            CheesecakeRun(2, 'Durus', '3.4.1', 'success'),
-            CheesecakeRun(3, 'cheesecake', '0.6', 'failure'),
-            CheesecakeRun(4, 'cheesecake', '0.6', 'failure'),
-        ]
-
-        store = Store()
-        for run in runs_list:
-            store.save_run(run)
-
-        assert 3 == store.number_of_failures('cheesecake', '0.6')
-
-        # Add another failure and see if counter rises.
-        store.save_run(CheesecakeRun(5, 'cheesecake', '0.6', 'failure'))
-        assert 4 == store.number_of_failures('cheesecake', '0.6')
-
-        # Add different package run.
-        store.save_run(CheesecakeRun(6, 'twill', '0.8.5', 'success'))
-        assert 4 == store.number_of_failures('cheesecake', '0.6')
-
-        # Add different version of the same package with failure.
-        store.save_run(CheesecakeRun(7, 'cheesecake', '0.7', 'failure'))
-        assert 4 == store.number_of_failures('cheesecake', '0.6')
-
-        # Add different version of the same package with success.
-        store.save_run(CheesecakeRun(7, 'cheesecake', '0.7', 'success'))
-        assert 4 == store.number_of_failures('cheesecake', '0.6')
-
-        # Adding a success for this release should reset the counter.
-        store.save_run(CheesecakeRun(7, 'cheesecake', '0.6', 'success'))
-        assert 0 == store.number_of_failures('cheesecake', '0.6')
-
-
 class DatabaseLogTest(TestCaseWithTestDatbase):
     def test_database_log(self):
         """Test Store.log and Store.get_messages.
         assert "[DEBUG] Hello world!" not in messages
         assert "[INFO] Bye bye!" in messages
 
-
-class CheesecakeScoreSerializeTest(unittest.TestCase):
-    def test_cheesecake_score_serialize(self):
-        """Test CheesecakeScore.serialize.
-        """
-        MainIndex = CheesecakeScore.MainIndex
-        SubIndex = CheesecakeScore.SubIndex
-
-        expected = {
-            'INSTALLABILITY': (40,
-                               10,
-                               [('unpack', 25, 'package unpacked successfully'),
-                               ('unpack_dir', 15, 'unpack directory is cheesecake-0.6 as expected')]),
-            'DOCUMENTATION': (214,
-                              20,
-                              [('required_files', 180, '6 files and 2 required directories found'),
-                              ('docstrings', 34, 'found 109/328=33.23% objects with docstrings'),
-                              ('formatted_docstrings', 0, 'found 20/328=6.10% objects with formatted docstrings')]),
-            'CODE_KWALITEE': (65,
-                              30,
-                              [('pylint', 35, 'pylint score was 7.00 out of 10'),
-                              ('unit_tested', 30, 'has unit tests')])
-        }
-
-        installability_sub = [
-            SubIndex('unpack', 25, 'package unpacked successfully'),
-            SubIndex('unpack_dir''', 15, 'unpack directory is cheesecake-0.6 as expected'),
-        ]
-        documentation_sub = [
-            SubIndex('required_files', 180, '6 files and 2 required directories found'),
-            SubIndex('docstrings', 34, 'found 109/328=33.23% objects with docstrings'),
-            SubIndex('formatted_docstrings', 0, 'found 20/328=6.10% objects with formatted docstrings'),
-        ]
-        code_kwalitee_sub = [
-            SubIndex('pylint', 35, 'pylint score was 7.00 out of 10'),
-            SubIndex('unit_tested', 30, 'has unit tests'),
-        ]
-
-        installability = MainIndex(40, 10, installability_sub)
-        documentation = MainIndex(214, 20, documentation_sub)
-        code_kwalitee = MainIndex(65, 30, code_kwalitee_sub)
-
-        score = CheesecakeScoreFromValues(installability, documentation, code_kwalitee)
-
-        serialized = score.serialize()
-
-        assert serialized == expected
-
 ################################################################################
 ## Main.
 ################################################################################