class TestItem(unittest.TestCase): """Unit tests for the Item class.""" # pylint: disable=W0212 def setUp(self): path = os.path.join('path', 'to', 'RQ001.yml') self.item = MockItem(path) def test_init_invalid(self): """Verify an item cannot be initialized from an invalid path.""" self.assertRaises(DoorstopError, Item, 'not/a/path') def test_object_references(self): """Verify a standalone item does not have object references.""" self.assertIs(None, self.item.document) self.assertIs(None, self.item.tree) def test_load_empty(self): """Verify loading calls read.""" self.item.load() self.item._read.assert_called_once_with(self.item.path) def test_load_error(self): """Verify an exception is raised with invalid YAML.""" self.item._file = "invalid: -" self.assertRaises(DoorstopError, self.item.load) def test_load_unexpected(self): """Verify an exception is raised for unexpected file contents.""" self.item._file = "unexpected" self.assertRaises(DoorstopError, self.item.load) def test_save_empty(self): """Verify saving calls write.""" self.item.save() self.item._write.assert_called_once_with(YAML_DEFAULT, self.item.path) @patch('doorstop.common.verbosity', 2) def test_str(self): """Verify an item can be converted to a string.""" self.assertEqual("RQ001", str(self.item)) @patch('doorstop.common.verbosity', 3) def test_str_verbose(self): """Verify an item can be converted to a string (verbose).""" text = "RQ001 (@{}{})".format(os.sep, self.item.path) self.assertEqual(text, str(self.item)) def test_hash(self): """Verify items can be hashed.""" item1 = MockItem('path/to/fake1.yml') item2 = MockItem('path/to/fake2.yml') item3 = MockItem('path/to/fake2.yml') my_set = set() # Act my_set.add(item1) my_set.add(item2) my_set.add(item3) # Assert self.assertEqual(2, len(my_set)) def test_ne(self): """Verify item non-equality is correct.""" self.assertNotEqual(self.item, None) def test_lt(self): """Verify items can be compared.""" item1 = MockItem('path/to/fake1.yml') item1.level = (1, 1) item2 = MockItem('path/to/fake1.yml') item2.level = (1, 1, 1) item3 = MockItem('path/to/fake1.yml') item3.level = (1, 1, 2) self.assertLess(item1, item2) self.assertLess(item2, item3) self.assertGreater(item3, item1) def test_uid(self): """Verify an item's UID can be read but not set.""" self.assertEqual('RQ001', self.item.uid) self.assertRaises(AttributeError, setattr, self.item, 'uid', 'RQ002') def test_relpath(self): """Verify an item's relative path string can be read but not set.""" text = "@{}{}".format(os.sep, self.item.path) self.assertEqual(text, self.item.relpath) self.assertRaises(AttributeError, setattr, self.item, 'relpath', '.') def test_prefix(self): """Verify an item's prefix can be read but not set.""" self.assertEqual('RQ', self.item.prefix) self.assertRaises(AttributeError, setattr, self.item, 'prefix', 'REQ') def test_number(self): """Verify an item's number can be read but not set.""" self.assertEqual(1, self.item.number) self.assertRaises(AttributeError, setattr, self.item, 'number', 2) def test_level(self): """Verify an item's level can be set and read.""" self.item.level = (1, 2, 3) self.assertIn("level: 1.2.3\n", self.item._write.call_args[0][0]) self.assertEqual((1, 2, 3), self.item.level) def test_level_with_float(self): """Verify an item's level can be set and read (2-part w/ float).""" self.item.level = (1, 10) self.assertIn("level: '1.10'\n", self.item._write.call_args[0][0]) self.assertEqual((1, 10), self.item.level) def test_depth(self): """Verify the depth can be read from the item's level.""" self.item.level = (1, ) self.assertEqual(1, self.item.depth) self.item.level = (1, 0) self.assertEqual(1, self.item.depth) self.item.level = (2, 0, 1) self.assertEqual(3, self.item.depth) self.item.level = (2, 0, 1, 1, 0, 0) self.assertEqual(4, self.item.depth) def test_level_from_text(self): """Verify an item's level can be set from text and read.""" self.item.level = "4.2.0 " self.assertIn("level: 4.2.0\n", self.item._write.call_args[0][0]) self.assertEqual((4, 2), self.item.level) def test_level_from_text_2_digits(self): """Verify an item's level can be set from text (2 digits) and read.""" self.item.level = "10.10" self.assertIn("level: '10.10'\n", self.item._write.call_args[0][0]) self.assertEqual((10, 10), self.item.level) def test_level_from_float(self): """Verify an item's level can be set from a float and read.""" self.item.level = 4.2 self.assertIn("level: 4.2\n", self.item._write.call_args[0][0]) self.assertEqual((4, 2), self.item.level) def test_level_from_int(self): """Verify an item's level can be set from a int and read.""" self.item.level = 42 self.assertIn("level: 42\n", self.item._write.call_args[0][0]) self.assertEqual((42, ), self.item.level) def test_active(self): """Verify an item's active status can be set and read.""" self.item.active = 0 # converted to False self.assertIn("active: false\n", self.item._write.call_args[0][0]) self.assertFalse(self.item.active) def test_derived(self): """Verify an item's normative status can be set and read.""" self.item.derived = 1 # converted to True self.assertIn("derived: true\n", self.item._write.call_args[0][0]) self.assertTrue(self.item.derived) def test_normative(self): """Verify an item's normative status can be set and read.""" self.item.normative = 0 # converted to False self.assertIn("normative: false\n", self.item._write.call_args[0][0]) self.assertFalse(self.item.normative) def test_heading(self): """Verify an item's heading status can be set and read.""" self.item.level = '1.1.1' self.item.heading = 1 # converted to True self.assertFalse(self.item.normative) self.assertTrue(self.item.heading) self.item.heading = 0 # converted to False self.assertTrue(self.item.normative) self.assertFalse(self.item.heading) def test_cleared(self): """Verify an item's suspect link status can be set and read.""" mock_item = Mock() mock_item.uid = 'mock_uid' mock_item.stamp = Mock(return_value=Stamp('abc123')) mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.tree = mock_tree self.item.link('mock_uid') self.item.cleared = 1 # updates each stamp self.assertTrue(self.item.cleared) self.item.cleared = 0 # sets each stamp to None self.assertFalse(self.item.cleared) def test_reviwed(self): """Verify an item's review status can be set and read.""" self.assertFalse(self.item.reviewed) # not reviewed by default self.item.reviewed = 1 # calls `review()` self.assertTrue(self.item.reviewed) self.item.reviewed = 0 # converted to None self.assertFalse(self.item.reviewed) def test_text(self): """Verify an item's text can be set and read.""" value = "abc " text = "abc" yaml = "text: |\n abc\n" self.item.text = value self.assertEqual(text, self.item.text) self.assertIn(yaml, self.item._write.call_args[0][0]) def test_text_sbd(self): """Verify newlines separate sentences in an item's text.""" value = ("A sentence. Another sentence! Hello? Hi.\n" "A new line (here). And another sentence.") text = ("A sentence. Another sentence! Hello? Hi. " "A new line (here). And another sentence.") yaml = ("text: |\n" " A sentence.\n" " Another sentence!\n" " Hello?\n" " Hi.\n" " A new line (here).\n" " And another sentence.\n") self.item.text = value self.assertEqual(text, self.item.text) self.assertIn(yaml, self.item._write.call_args[0][0]) def test_text_ordered_list(self): """Verify newlines are preserved in an ordered list.""" self.item.text = "A list:\n\n1. Abc\n2. Def\n" expected = "A list:\n\n1. Abc\n2. Def" self.assertEqual(expected, self.item.text) def test_text_unordered_list(self): """Verify newlines are preserved in an ordered list.""" self.item.text = "A list:\n\n- Abc\n- Def\n" expected = "A list:\n\n- Abc\n- Def" self.assertEqual(expected, self.item.text) def test_text_split_numbers(self): """Verify lines ending in numbers are joined correctly.""" self.item.text = "Split at a number: 1\n42 or punctuation.\nHere." expected = "Split at a number: 1 42 or punctuation. Here." self.assertEqual(expected, self.item.text) def test_text_newlines(self): """Verify newlines are preserved when deliberate.""" self.item.text = "Some text.\n\nNote: here.\n" expected = "Some text.\n\nNote: here." self.assertEqual(expected, self.item.text) def test_text_formatting(self): """Verify newlines are removed around formatting.""" self.item.text = "The thing\n**_SHALL_** do this.\n" expected = "The thing **_SHALL_** do this." self.assertEqual(expected, self.item.text) def test_text_non_heading(self): """Verify newlines are removed around non-headings.""" self.item.text = "break (before \n#2) symbol should not be a heading." expected = "break (before #2) symbol should not be a heading." self.assertEqual(expected, self.item.text) def test_text_heading(self): """Verify newlines are preserved around headings.""" self.item.text = "should be a heading\n\n# right here" expected = "should be a heading\n\n# right here" self.assertEqual(expected, self.item.text) def test_ref(self): """Verify an item's reference can be set and read.""" self.item.ref = "abc123" self.assertIn("ref: abc123\n", self.item._write.call_args[0][0]) self.assertEqual("abc123", self.item.ref) def test_extended(self): """Verify an extended attribute (`str`) can be used.""" self.item.set('ext1', 'foobar') self.assertIn("ext1: foobar\n", self.item._write.call_args[0][0]) self.assertEqual('foobar', self.item.get('ext1')) self.assertEqual(['ext1'], self.item.extended) def test_extended_text(self): """Verify an extended attribute (`Text`) can be used.""" self.item.set('ext1', Text('foobar')) self.assertIn("ext1: foobar\n", self.item._write.call_args[0][0]) self.assertEqual('foobar', self.item.get('ext1')) self.assertEqual(['ext1'], self.item.extended) def test_extended_wrap(self): """Verify a long extended attribute is wrapped.""" text = "This extended attribute should be long enough to wrap." self.item.set('a_very_long_extended_attr', text) self.assertEqual(text, self.item.get('a_very_long_extended_attr')) def test_extended_wrap_multi(self): """Verify a long extended attribute is wrapped with newlines.""" text = "Another extended attribute.\n\nNote: with a note." self.item.set('ext2', text) self.assertEqual(text, self.item.get('ext2')) def test_extended_get_standard(self): """Verify extended attribute access can get standard properties.""" active = self.item.get('active') self.assertEqual(self.item.active, active) def test_extended_set_standard(self): """Verify extended attribute access can set standard properties.""" self.item.set('text', "extended access") self.assertEqual("extended access", self.item.text) @patch('doorstop.core.editor.launch') def test_edit(self, mock_launch): """Verify an item can be edited.""" self.item.tree = Mock() # Act self.item.edit(tool='mock_editor') # Assert self.item.tree.vcs.lock.assert_called_once_with(self.item.path) self.item.tree.vcs.edit.assert_called_once_with(self.item.path) mock_launch.assert_called_once_with(self.item.path, tool='mock_editor') def test_link(self): """Verify links can be added to an item.""" self.item.link('abc') self.item.link('123') self.assertEqual(['123', 'abc'], self.item.links) def test_link_duplicate(self): """Verify duplicate links are ignored.""" self.item.link('abc') self.item.link('abc') self.assertEqual(['abc'], self.item.links) def test_unlink_duplicate(self): """Verify removing a link twice is not an error.""" self.item.links = ['123', 'abc'] self.item.unlink('abc') self.item.unlink('abc') self.assertEqual(['123'], self.item.links) def test_link_by_item(self): """Verify links can be added to an item (by item).""" path = os.path.join('path', 'to', 'ABC123.yml') item = MockItem(path) self.item.link(item) self.assertEqual(['ABC123'], self.item.links) def test_unlink_by_item(self): """Verify links can be removed (by item).""" path = os.path.join('path', 'to', 'ABC123.yml') item = MockItem(path) self.item.links = ['ABC123'] self.item.unlink(item) self.assertEqual([], self.item.links) def test_links_alias(self): """Verify 'parent_links' is an alias for links.""" links1 = ['alias1'] links2 = ['alias2'] self.item.parent_links = links1 self.assertEqual(links1, self.item.links) self.item.links = links2 self.assertEqual(links2, self.item.parent_links) def test_parent_items(self): """Verify 'parent_items' exists to mirror the child behavior.""" mock_tree = Mock() mock_tree.find_item = Mock(return_value='mock_item') self.item.tree = mock_tree self.item.links = ['mock_uid'] # Act items = self.item.parent_items # Assert self.assertEqual(['mock_item'], items) def test_parent_items_unknown(self): """Verify 'parent_items' can handle unknown items.""" mock_tree = Mock() mock_tree.find_item = Mock(side_effect=DoorstopError) self.item.tree = mock_tree self.item.links = ['mock_uid'] # Act items = self.item.parent_items # Assert self.assertIsInstance(items[0], UnknownItem) def test_parent_documents(self): """Verify 'parent_documents' exists to mirror the child behavior.""" mock_tree = Mock() mock_tree.find_document = Mock(return_value='mock_document') self.item.tree = mock_tree self.item.links = ['mock_uid'] self.item.document = Mock() self.item.document.prefix = 'mock_prefix' # Act documents = self.item.parent_documents # Assert self.assertEqual(['mock_document'], documents) def test_parent_documents_unknown(self): """Verify 'parent_documents' can handle unknown documents.""" mock_tree = Mock() mock_tree.find_document = Mock(side_effect=DoorstopError) self.item.tree = mock_tree self.item.links = ['mock_uid'] self.item.document = Mock() self.item.document.prefix = 'mock_prefix' # Act documents = self.item.parent_documents # Assert self.assertEqual([], documents) def test_parent_documents_no_document(self): """Verify 'parent_documents' is only valid with a document.""" self.item.tree = Mock() self.assertIs(None, self.item.parent_documents) @patch('doorstop.settings.CACHE_PATHS', False) def test_find_ref(self): """Verify an item's reference can be found.""" self.item.ref = "REF" "123" # space to avoid matching in this file self.item.tree = Mock() self.item.tree.vcs = WorkingCopy(EXTERNAL) # Act relpath, line = self.item.find_ref() # Assert self.assertEqual('text.txt', os.path.basename(relpath)) self.assertEqual(3, line) def test_find_ref_filename(self): """Verify an item's reference can also be a filename.""" self.item.ref = "text.txt" self.item.tree = Mock() self.item.tree.vcs = WorkingCopy(FILES) self.item.tree.vcs._ignores_cache = ["*published*"] # Act relpath, line = self.item.find_ref() # Assert self.assertEqual('text.txt', os.path.basename(relpath)) self.assertEqual(None, line) def test_find_ref_error(self): """Verify an error occurs when no external reference found.""" self.item.ref = "not" "found" # space to avoid matching in this file self.item.tree = Mock() self.item.tree.vcs = WorkingCopy(EMPTY) # Act and assert self.assertRaises(DoorstopError, self.item.find_ref) def test_find_skip_self(self): """Verify reference searches skip the item's file.""" self.item.path = __file__ self.item.ref = "148710938710289248" # random and unique to this file self.item.tree = Mock() self.item.tree.vcs = WorkingCopy(EMPTY) self.item.tree.vcs._path_cache = [(__file__, 'filename', 'relpath')] # Act and assert self.assertRaises(DoorstopError, self.item.find_ref) def test_find_ref_none(self): """Verify nothing returned when no external reference is specified.""" self.item.tree = Mock() self.assertEqual((None, None), self.item.find_ref()) def test_find_child_objects(self): """Verify an item's child objects can be found.""" mock_document_p = Mock() mock_document_p.prefix = 'RQ' mock_document_c = Mock() mock_document_c.parent = 'RQ' mock_item = Mock() mock_item.uid = 'TST001' mock_item.links = ['RQ001'] def mock_iter(self): # pylint: disable=W0613 """Mock Tree.__iter__ to yield a mock Document.""" def mock_iter2(self): # pylint: disable=W0613 """Mock Document.__iter__ to yield a mock Item.""" yield mock_item mock_document_c.__iter__ = mock_iter2 yield mock_document_c self.item.link('fake1') mock_tree = Mock() mock_tree.__iter__ = mock_iter mock_tree.find_item = lambda uid: Mock(uid='fake1') self.item.tree = mock_tree self.item.document = mock_document_p links = self.item.find_child_links() items = self.item.find_child_items() documents = self.item.find_child_documents() self.assertEqual(['TST001'], links) self.assertEqual([mock_item], items) self.assertEqual([mock_document_c], documents) def test_find_child_objects_standalone(self): """Verify a standalone item has no child objects.""" self.assertEqual([], self.item.child_links) self.assertEqual([], self.item.child_items) self.assertEqual([], self.item.child_documents) def test_invalid_file_name(self): """Verify an invalid file name cannot be a requirement.""" self.assertRaises(DoorstopError, MockItem, "path/to/REQ.yaml") self.assertRaises(DoorstopError, MockItem, "path/to/001.yaml") def test_invalid_file_ext(self): """Verify an invalid file extension cannot be a requirement.""" self.assertRaises(DoorstopError, MockItem, "path/to/REQ001") self.assertRaises(DoorstopError, MockItem, "path/to/REQ001.txt") @patch('doorstop.core.item.Item', MockItem) def test_new(self): """Verify items can be created.""" MockItem._create.reset_mock() item = MockItem.new(None, None, EMPTY, FILES, 'TEST00042', level=(1, 2, 3)) path = os.path.join(EMPTY, 'TEST00042.yml') self.assertEqual(path, item.path) self.assertEqual((1, 2, 3), item.level) MockItem._create.assert_called_once_with(path, name='item') @patch('doorstop.core.item.Item', MockItem) def test_new_cache(self): """Verify new items are cached.""" mock_tree = Mock() mock_tree._item_cache = {} item = MockItem.new(mock_tree, None, EMPTY, FILES, 'TEST00042', level=(1, 2, 3)) self.assertEqual(item, mock_tree._item_cache[item.uid]) mock_tree.vcs.add.assert_called_once_with(item.path) @patch('doorstop.core.item.Item', MockItem) def test_new_special(self): """Verify items can be created with a specially named prefix.""" MockItem._create.reset_mock() item = MockItem.new(None, None, EMPTY, FILES, 'VSM.HLR_01-002-042', level=(1, 0)) path = os.path.join(EMPTY, 'VSM.HLR_01-002-042.yml') self.assertEqual(path, item.path) self.assertEqual((1, ), item.level) MockItem._create.assert_called_once_with(path, name='item') def test_new_existing(self): """Verify an exception is raised if the item already exists.""" self.assertRaises(DoorstopError, Item.new, None, None, FILES, FILES, 'REQ002', level=(1, 2, 3)) def test_validate_invalid_ref(self): """Verify an invalid reference fails validity.""" with patch('doorstop.core.item.Item.find_ref', Mock(side_effect=DoorstopError)): self.assertFalse(self.item.validate()) def test_validate_inactive(self): """Verify an inactive item is not checked.""" self.item.active = False with patch('doorstop.core.item.Item.find_ref', Mock(side_effect=DoorstopError)): self.assertTrue(self.item.validate()) def test_validate_reviewed(self): """Verify that checking a reviewed item updates the stamp.""" self.item._data['reviewed'] = True self.assertTrue(self.item.validate()) stamp = 'c6a87755b8756b61731c704c6a7be4a2' self.assertEqual(stamp, self.item._data['reviewed']) def test_validate_cleared(self): """Verify that checking a cleared link updates the stamp.""" mock_item = Mock() mock_item.stamp = Mock(return_value=Stamp('abc123')) mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.tree = mock_tree self.item.links = [{'mock_uid': True}] self.assertTrue(self.item.validate()) self.assertEqual('abc123', self.item.links[0].stamp) def test_validate_nonnormative_with_links(self): """Verify a non-normative item with links can be checked.""" self.item.normative = False self.item.links = ['a'] self.assertTrue(self.item.validate()) def test_validate_link_to_inactive(self): """Verify a link to an inactive item can be checked.""" mock_item = Mock() mock_item.active = False mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.links = ['a'] self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_link_to_nonnormative(self): """Verify a link to an non-normative item can be checked.""" mock_item = Mock() mock_item.normative = False mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.links = ['a'] self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_document(self): """Verify an item can be checked against a document.""" mock_document = Mock() mock_document.parent = 'fake' self.item.document = mock_document self.assertTrue(self.item.validate()) def test_validate_document_with_links(self): """Verify an item can be checked against a document with links.""" self.item.link('unknown1') mock_document = Mock() mock_document.parent = 'fake' self.item.document = mock_document self.assertTrue(self.item.validate()) def test_validate_document_with_bad_link_uids(self): """Verify an item can be checked against a document w/ bad links.""" self.item.link('invalid') mock_document = Mock() mock_document.parent = 'fake' self.item.document = mock_document self.assertFalse(self.item.validate()) def test_validate_tree(self): """Verify an item can be checked against a tree.""" def mock_iter(self): # pylint: disable=W0613 """Mock Tree.__iter__ to yield a mock Document.""" mock_document = Mock() mock_document.parent = 'RQ' def mock_iter2(self): # pylint: disable=W0613 """Mock Document.__iter__ to yield a mock Item.""" mock_item = Mock() mock_item.uid = 'TST001' mock_item.links = ['RQ001'] yield mock_item mock_document.__iter__ = mock_iter2 yield mock_document self.item.link('fake1') mock_tree = Mock() mock_tree.__iter__ = mock_iter mock_tree.find_item = lambda uid: Mock(uid='fake1') self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_tree_error(self): """Verify an item can be checked against a tree with errors.""" self.item.link('fake1') mock_tree = MagicMock() mock_tree.find_item = Mock(side_effect=DoorstopError) self.item.tree = mock_tree self.assertFalse(self.item.validate()) def test_validate_both(self): """Verify an item can be checked against both.""" def mock_iter(seq): """Creates a mock __iter__ method.""" def _iter(self): # pylint: disable=W0613 """Mock __iter__method.""" yield from seq return _iter mock_item = Mock() mock_item.links = [self.item.uid] mock_document = Mock() mock_document.parent = 'BOTH' mock_document.prefix = 'BOTH' mock_document.__iter__ = mock_iter([mock_item]) mock_tree = Mock() mock_tree.__iter__ = mock_iter([mock_document]) self.item.document = mock_document self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_both_no_reverse_links(self): """Verify an item can be checked against both (no reverse links).""" def mock_iter(self): # pylint: disable=W0613 """Mock Tree.__iter__ to yield a mock Document.""" mock_document = Mock() mock_document.parent = 'RQ' def mock_iter2(self): # pylint: disable=W0613 """Mock Document.__iter__ to yield a mock Item.""" mock_item = Mock() mock_item.uid = 'TST001' mock_item.links = [] yield mock_item mock_document.__iter__ = mock_iter2 yield mock_document self.item.link('fake1') mock_document = Mock() mock_document.prefix = 'RQ' mock_tree = Mock() mock_tree.__iter__ = mock_iter mock_tree.find_item = lambda uid: Mock(uid='fake1') self.item.document = mock_document self.item.tree = mock_tree self.assertTrue(self.item.validate()) @patch('doorstop.core.item.Item.get_issues', Mock(return_value=[])) def test_issues(self): """Verify an item's issues convenience property can be accessed.""" self.assertEqual(0, len(self.item.issues)) def test_stamp(self): """Verify an item's contents can be stamped.""" stamp = 'c6a87755b8756b61731c704c6a7be4a2' self.assertEqual(stamp, self.item.stamp()) def test_stamp_links(self): """Verify an item's contents can be stamped.""" self.item.link('mock_link') stamp = '1020719292bbdc4090bd236cf41cd104' self.assertEqual(stamp, self.item.stamp(links=True)) def test_clear(self): """Verify an item's links can be cleared as suspect.""" mock_item = Mock() mock_item.uid = 'mock_uid' mock_item.stamp = Mock(return_value=Stamp('abc123')) mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.tree = mock_tree self.item.link('mock_uid') self.assertFalse(self.item.cleared) self.assertEqual(None, self.item.links[0].stamp) # Act self.item.clear() # Assert self.assertTrue(self.item.cleared) self.assertEqual('abc123', self.item.links[0].stamp) def test_review(self): """Verify an item can be marked as reviewed.""" self.item.reviewed = False self.item.review() self.assertTrue(self.item.reviewed) @patch('doorstop.common.delete') def test_delete(self, mock_delete): """Verify an item can be deleted.""" self.item.delete() mock_delete.assert_called_once_with(self.item.path) self.item.delete() # ensure a second delete is ignored @patch('doorstop.common.delete', Mock()) def test_delete_cache(self): """Verify an item is expunged after delete.""" self.item.tree = Mock() self.item.tree._item_cache = {self.item.uid: self.item} self.item.delete() self.item.tree.vcs.delete.assert_called_once_with(self.item.path) self.assertIs(None, self.item.tree._item_cache[self.item.uid])
class TestItem(unittest.TestCase): """Unit tests for the Item class.""" # pylint: disable=W0212 def setUp(self): path = os.path.join('path', 'to', 'RQ001.yml') self.item = MockItem(path) def test_init_invalid(self): """Verify an item cannot be initialized from an invalid path.""" self.assertRaises(DoorstopError, Item, 'not/a/path') def test_object_references(self): """Verify a standalone item does not have object references.""" self.assertIs(None, self.item.document) self.assertIs(None, self.item.tree) def test_load_empty(self): """Verify loading calls read.""" self.item.load() self.item._read.assert_called_once_with(self.item.path) def test_load_error(self): """Verify an exception is raised with invalid YAML.""" self.item._file = "invalid: -" self.assertRaises(DoorstopError, self.item.load) def test_load_unexpected(self): """Verify an exception is raised for unexpected file contents.""" self.item._file = "unexpected" self.assertRaises(DoorstopError, self.item.load) def test_save_empty(self): """Verify saving calls write.""" self.item.save() self.item._write.assert_called_once_with(YAML_DEFAULT, self.item.path) @patch('doorstop.common.verbosity', 2) def test_str(self): """Verify an item can be converted to a string.""" self.assertEqual("RQ001", str(self.item)) @patch('doorstop.common.verbosity', 3) def test_str_verbose(self): """Verify an item can be converted to a string (verbose).""" text = "RQ001 (@{}{})".format(os.sep, self.item.path) self.assertEqual(text, str(self.item)) def test_hash(self): """Verify items can be hashed.""" item1 = MockItem('path/to/fake1.yml') item2 = MockItem('path/to/fake2.yml') item3 = MockItem('path/to/fake2.yml') my_set = set() # Act my_set.add(item1) my_set.add(item2) my_set.add(item3) # Assert self.assertEqual(2, len(my_set)) def test_ne(self): """Verify item non-equality is correct.""" self.assertNotEqual(self.item, None) def test_lt(self): """Verify items can be compared.""" item1 = MockItem('path/to/fake1.yml') item1.level = (1, 1) item2 = MockItem('path/to/fake1.yml') item2.level = (1, 1, 1) item3 = MockItem('path/to/fake1.yml') item3.level = (1, 1, 2) self.assertLess(item1, item2) self.assertLess(item2, item3) self.assertGreater(item3, item1) def test_uid(self): """Verify an item's UID can be read but not set.""" self.assertEqual('RQ001', self.item.uid) self.assertRaises(AttributeError, setattr, self.item, 'uid', 'RQ002') def test_relpath(self): """Verify an item's relative path string can be read but not set.""" text = "@{}{}".format(os.sep, self.item.path) self.assertEqual(text, self.item.relpath) self.assertRaises(AttributeError, setattr, self.item, 'relpath', '.') def test_prefix(self): """Verify an item's prefix can be read but not set.""" self.assertEqual('RQ', self.item.prefix) self.assertRaises(AttributeError, setattr, self.item, 'prefix', 'REQ') def test_number(self): """Verify an item's number can be read but not set.""" self.assertEqual(1, self.item.number) self.assertRaises(AttributeError, setattr, self.item, 'number', 2) def test_level(self): """Verify an item's level can be set and read.""" self.item.level = (1, 2, 3) self.assertIn("level: 1.2.3\n", self.item._write.call_args[0][0]) self.assertEqual((1, 2, 3), self.item.level) def test_level_with_float(self): """Verify an item's level can be set and read (2-part w/ float).""" self.item.level = (1, 10) self.assertIn("level: '1.10'\n", self.item._write.call_args[0][0]) self.assertEqual((1, 10), self.item.level) def test_depth(self): """Verify the depth can be read from the item's level.""" self.item.level = (1,) self.assertEqual(1, self.item.depth) self.item.level = (1, 0) self.assertEqual(1, self.item.depth) self.item.level = (2, 0, 1) self.assertEqual(3, self.item.depth) self.item.level = (2, 0, 1, 1, 0, 0) self.assertEqual(4, self.item.depth) def test_level_from_text(self): """Verify an item's level can be set from text and read.""" self.item.level = "4.2.0 " self.assertIn("level: 4.2.0\n", self.item._write.call_args[0][0]) self.assertEqual((4, 2), self.item.level) def test_level_from_text_2_digits(self): """Verify an item's level can be set from text (2 digits) and read.""" self.item.level = "10.10" self.assertIn("level: '10.10'\n", self.item._write.call_args[0][0]) self.assertEqual((10, 10), self.item.level) def test_level_from_float(self): """Verify an item's level can be set from a float and read.""" self.item.level = 4.2 self.assertIn("level: 4.2\n", self.item._write.call_args[0][0]) self.assertEqual((4, 2), self.item.level) def test_level_from_int(self): """Verify an item's level can be set from a int and read.""" self.item.level = 42 self.assertIn("level: 42\n", self.item._write.call_args[0][0]) self.assertEqual((42,), self.item.level) def test_active(self): """Verify an item's active status can be set and read.""" self.item.active = 0 # converted to False self.assertIn("active: false\n", self.item._write.call_args[0][0]) self.assertFalse(self.item.active) def test_derived(self): """Verify an item's normative status can be set and read.""" self.item.derived = 1 # converted to True self.assertIn("derived: true\n", self.item._write.call_args[0][0]) self.assertTrue(self.item.derived) def test_normative(self): """Verify an item's normative status can be set and read.""" self.item.normative = 0 # converted to False self.assertIn("normative: false\n", self.item._write.call_args[0][0]) self.assertFalse(self.item.normative) def test_heading(self): """Verify an item's heading status can be set and read.""" self.item.level = '1.1.1' self.item.heading = 1 # converted to True self.assertFalse(self.item.normative) self.assertTrue(self.item.heading) self.item.heading = 0 # converted to False self.assertTrue(self.item.normative) self.assertFalse(self.item.heading) def test_cleared(self): """Verify an item's suspect link status can be set and read.""" mock_item = Mock() mock_item.uid = 'mock_uid' mock_item.stamp = Mock(return_value=Stamp('abc123')) mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.tree = mock_tree self.item.link('mock_uid') self.item.cleared = 1 # updates each stamp self.assertTrue(self.item.cleared) self.item.cleared = 0 # sets each stamp to None self.assertFalse(self.item.cleared) def test_reviwed(self): """Verify an item's review status can be set and read.""" self.assertFalse(self.item.reviewed) # not reviewed by default self.item.reviewed = 1 # calls `review()` self.assertTrue(self.item.reviewed) self.item.reviewed = 0 # converted to None self.assertFalse(self.item.reviewed) def test_text(self): """Verify an item's text can be set and read.""" value = "abc " text = "abc" yaml = "text: |\n abc\n" self.item.text = value self.assertEqual(text, self.item.text) self.assertIn(yaml, self.item._write.call_args[0][0]) def test_text_sbd(self): """Verify newlines separate sentences in an item's text.""" value = ("A sentence. Another sentence! Hello? Hi.\n" "A new line (here). And another sentence.") text = ("A sentence. Another sentence! Hello? Hi. " "A new line (here). And another sentence.") yaml = ("text: |\n" " A sentence.\n" " Another sentence!\n" " Hello?\n" " Hi.\n" " A new line (here).\n" " And another sentence.\n") self.item.text = value self.assertEqual(text, self.item.text) self.assertIn(yaml, self.item._write.call_args[0][0]) def test_text_ordered_list(self): """Verify newlines are preserved in an ordered list.""" self.item.text = "A list:\n\n1. Abc\n2. Def\n" expected = "A list:\n\n1. Abc\n2. Def" self.assertEqual(expected, self.item.text) def test_text_unordered_list(self): """Verify newlines are preserved in an ordered list.""" self.item.text = "A list:\n\n- Abc\n- Def\n" expected = "A list:\n\n- Abc\n- Def" self.assertEqual(expected, self.item.text) def test_text_split_numbers(self): """Verify lines ending in numbers are joined correctly.""" self.item.text = "Split at a number: 1\n42 or punctuation.\nHere." expected = "Split at a number: 1 42 or punctuation. Here." self.assertEqual(expected, self.item.text) def test_text_newlines(self): """Verify newlines are preserved when deliberate.""" self.item.text = "Some text.\n\nNote: here.\n" expected = "Some text.\n\nNote: here." self.assertEqual(expected, self.item.text) def test_text_formatting(self): """Verify newlines are removed around formatting.""" self.item.text = "The thing\n**_SHALL_** do this.\n" expected = "The thing **_SHALL_** do this." self.assertEqual(expected, self.item.text) def test_text_non_heading(self): """Verify newlines are removed around non-headings.""" self.item.text = "break (before \n#2) symbol should not be a heading." expected = "break (before #2) symbol should not be a heading." self.assertEqual(expected, self.item.text) def test_text_heading(self): """Verify newlines are preserved around headings.""" self.item.text = "should be a heading\n\n# right here" expected = "should be a heading\n\n# right here" self.assertEqual(expected, self.item.text) def test_ref(self): """Verify an item's reference can be set and read.""" self.item.ref = "abc123" self.assertIn("ref: abc123\n", self.item._write.call_args[0][0]) self.assertEqual("abc123", self.item.ref) def test_extended(self): """Verify an extended attribute (`str`) can be used.""" self.item.set('ext1', 'foobar') self.assertIn("ext1: foobar\n", self.item._write.call_args[0][0]) self.assertEqual('foobar', self.item.get('ext1')) self.assertEqual(['ext1'], self.item.extended) def test_extended_text(self): """Verify an extended attribute (`Text`) can be used.""" self.item.set('ext1', Text('foobar')) self.assertIn("ext1: foobar\n", self.item._write.call_args[0][0]) self.assertEqual('foobar', self.item.get('ext1')) self.assertEqual(['ext1'], self.item.extended) def test_extended_wrap(self): """Verify a long extended attribute is wrapped.""" text = "This extended attribute should be long enough to wrap." self.item.set('a_very_long_extended_attr', text) self.assertEqual(text, self.item.get('a_very_long_extended_attr')) def test_extended_wrap_multi(self): """Verify a long extended attribute is wrapped with newlines.""" text = "Another extended attribute.\n\nNote: with a note." self.item.set('ext2', text) self.assertEqual(text, self.item.get('ext2')) def test_extended_get_standard(self): """Verify extended attribute access can get standard properties.""" active = self.item.get('active') self.assertEqual(self.item.active, active) def test_extended_set_standard(self): """Verify extended attribute access can set standard properties.""" self.item.set('text', "extended access") self.assertEqual("extended access", self.item.text) @patch('doorstop.core.editor.launch') def test_edit(self, mock_launch): """Verify an item can be edited.""" self.item.tree = Mock() # Act self.item.edit(tool='mock_editor') # Assert self.item.tree.vcs.lock.assert_called_once_with(self.item.path) mock_launch.assert_called_once_with(self.item.path, tool='mock_editor') def test_link(self): """Verify links can be added to an item.""" self.item.link('abc') self.item.link('123') self.assertEqual(['123', 'abc'], self.item.links) def test_link_duplicate(self): """Verify duplicate links are ignored.""" self.item.link('abc') self.item.link('abc') self.assertEqual(['abc'], self.item.links) def test_unlink_duplicate(self): """Verify removing a link twice is not an error.""" self.item.links = ['123', 'abc'] self.item.unlink('abc') self.item.unlink('abc') self.assertEqual(['123'], self.item.links) def test_link_by_item(self): """Verify links can be added to an item (by item).""" path = os.path.join('path', 'to', 'ABC123.yml') item = MockItem(path) self.item.link(item) self.assertEqual(['ABC123'], self.item.links) def test_unlink_by_item(self): """Verify links can be removed (by item).""" path = os.path.join('path', 'to', 'ABC123.yml') item = MockItem(path) self.item.links = ['ABC123'] self.item.unlink(item) self.assertEqual([], self.item.links) def test_links_alias(self): """Verify 'parent_links' is an alias for links.""" links1 = ['alias1'] links2 = ['alias2'] self.item.parent_links = links1 self.assertEqual(links1, self.item.links) self.item.links = links2 self.assertEqual(links2, self.item.parent_links) def test_parent_items(self): """Verify 'parent_items' exists to mirror the child behavior.""" mock_tree = Mock() mock_tree.find_item = Mock(return_value='mock_item') self.item.tree = mock_tree self.item.links = ['mock_uid'] # Act items = self.item.parent_items # Assert self.assertEqual(['mock_item'], items) def test_parent_items_unknown(self): """Verify 'parent_items' can handle unknown items.""" mock_tree = Mock() mock_tree.find_item = Mock(side_effect=DoorstopError) self.item.tree = mock_tree self.item.links = ['mock_uid'] # Act items = self.item.parent_items # Assert self.assertIsInstance(items[0], UnknownItem) def test_parent_documents(self): """Verify 'parent_documents' exists to mirror the child behavior.""" # Arrange mock_tree = Mock() mock_tree.find_document = Mock(return_value='mock_document') self.item.tree = mock_tree self.item.links = ['mock_uid'] self.item.document = Mock() self.item.document.prefix = 'mock_prefix' # Act documents = self.item.parent_documents # Assert self.assertEqual(['mock_document'], documents) def test_parent_documents_unknown(self): """Verify 'parent_documents' can handle unknown documents.""" # Arrange mock_tree = Mock() mock_tree.find_document = Mock(side_effect=DoorstopError) self.item.tree = mock_tree self.item.links = ['mock_uid'] self.item.document = Mock() self.item.document.prefix = 'mock_prefix' # Act documents = self.item.parent_documents # Assert self.assertEqual([], documents) def test_find_ref(self): """Verify an item's reference can be found.""" self.item.ref = "REF" + "123" # to avoid matching in this file relpath, line = self.item.find_ref(root=EXTERNAL) self.assertEqual('text.txt', os.path.basename(relpath)) self.assertEqual(3, line) def test_find_ref_filename(self): """Verify an item's reference can also be a filename.""" def skip(path): """Skip generated files.""" return "published" in path self.item.ref = "text.txt" relpath, line = self.item.find_ref(root=FILES, skip=skip) self.assertEqual('text.txt', os.path.basename(relpath)) self.assertEqual(None, line) def test_find_ref_error(self): """Verify an error occurs when no external reference found.""" self.item.ref = "not found".replace(' ', '') # avoids self match self.assertRaises(DoorstopError, self.item.find_ref, root=EMPTY) def test_find_ref_none(self): """Verify nothing returned when no external reference is specified.""" self.assertEqual((None, None), self.item.find_ref()) def test_find_child_objects(self): """Verify an item's child objects can be found.""" mock_document_p = Mock() mock_document_p.prefix = 'RQ' mock_document_c = Mock() mock_document_c.parent = 'RQ' mock_item = Mock() mock_item.uid = 'TST001' mock_item.links = ['RQ001'] def mock_iter(self): # pylint: disable=W0613 """Mock Tree.__iter__ to yield a mock Document.""" def mock_iter2(self): # pylint: disable=W0613 """Mock Document.__iter__ to yield a mock Item.""" yield mock_item mock_document_c.__iter__ = mock_iter2 yield mock_document_c self.item.link('fake1') mock_tree = Mock() mock_tree.__iter__ = mock_iter mock_tree.find_item = lambda uid: Mock(uid='fake1') self.item.tree = mock_tree self.item.document = mock_document_p links = self.item.find_child_links() items = self.item.find_child_items() documents = self.item.find_child_documents() self.assertEqual(['TST001'], links) self.assertEqual([mock_item], items) self.assertEqual([mock_document_c], documents) def test_find_child_objects_standalone(self): """Verify a standalone item has no child objects.""" self.assertEqual([], self.item.child_links) self.assertEqual([], self.item.child_items) self.assertEqual([], self.item.child_documents) def test_invalid_file_name(self): """Verify an invalid file name cannot be a requirement.""" self.assertRaises(DoorstopError, MockItem, "path/to/REQ.yaml") self.assertRaises(DoorstopError, MockItem, "path/to/001.yaml") def test_invalid_file_ext(self): """Verify an invalid file extension cannot be a requirement.""" self.assertRaises(DoorstopError, MockItem, "path/to/REQ001") self.assertRaises(DoorstopError, MockItem, "path/to/REQ001.txt") @patch('doorstop.core.item.Item', MockItem) def test_new(self): """Verify items can be created.""" MockItem._create.reset_mock() item = MockItem.new(None, None, EMPTY, FILES, 'TEST00042', level=(1, 2, 3)) path = os.path.join(EMPTY, 'TEST00042.yml') self.assertEqual(path, item.path) self.assertEqual((1, 2, 3), item.level) MockItem._create.assert_called_once_with(path, name='item') @patch('doorstop.core.item.Item', MockItem) def test_new_cache(self): """Verify new items are cached.""" mock_tree = Mock() mock_tree._item_cache = {} item = MockItem.new(mock_tree, None, EMPTY, FILES, 'TEST00042', level=(1, 2, 3)) self.assertEqual(item, mock_tree._item_cache[item.uid]) @patch('doorstop.core.item.Item', MockItem) def test_new_special(self): """Verify items can be created with a specially named prefix.""" MockItem._create.reset_mock() item = MockItem.new(None, None, EMPTY, FILES, 'VSM.HLR_01-002-042', level=(1, 0)) path = os.path.join(EMPTY, 'VSM.HLR_01-002-042.yml') self.assertEqual(path, item.path) self.assertEqual((1,), item.level) MockItem._create.assert_called_once_with(path, name='item') def test_new_existing(self): """Verify an exception is raised if the item already exists.""" self.assertRaises(DoorstopError, Item.new, None, None, FILES, FILES, 'REQ002', level=(1, 2, 3)) def test_validate_invalid_ref(self): """Verify an invalid reference fails validity.""" with patch('doorstop.core.item.Item.find_ref', Mock(side_effect=DoorstopError)): self.assertFalse(self.item.validate()) def test_validate_inactive(self): """Verify an inactive item is not checked.""" self.item.active = False with patch('doorstop.core.item.Item.find_ref', Mock(side_effect=DoorstopError)): self.assertTrue(self.item.validate()) def test_validate_reviewed(self): """Verify that checking a reviewed item updates the stamp.""" self.item._data['reviewed'] = True self.assertTrue(self.item.validate()) stamp = 'c6a87755b8756b61731c704c6a7be4a2' self.assertEqual(stamp, self.item._data['reviewed']) def test_validate_cleared(self): """Verify that checking a cleared link updates the stamp.""" mock_item = Mock() mock_item.stamp = Mock(return_value=Stamp('abc123')) mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.tree = mock_tree self.item.links = [{'mock_uid': True}] self.assertTrue(self.item.validate()) self.assertEqual('abc123', self.item.links[0].stamp) # pylint: disable=E1101 def test_validate_nonnormative_with_links(self): """Verify a non-normative item with links can be checked.""" self.item.normative = False self.item.links = ['a'] self.assertTrue(self.item.validate()) def test_validate_link_to_inactive(self): """Verify a link to an inactive item can be checked.""" mock_item = Mock() mock_item.active = False mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.links = ['a'] self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_link_to_nonnormative(self): """Verify a link to an non-normative item can be checked.""" mock_item = Mock() mock_item.normative = False mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.links = ['a'] self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_document(self): """Verify an item can be checked against a document.""" mock_document = Mock() mock_document.parent = 'fake' self.item.document = mock_document self.assertTrue(self.item.validate()) def test_validate_document_with_links(self): """Verify an item can be checked against a document with links.""" self.item.link('unknown1') mock_document = Mock() mock_document.parent = 'fake' self.item.document = mock_document self.assertTrue(self.item.validate()) def test_validate_document_with_bad_link_uids(self): """Verify an item can be checked against a document w/ bad links.""" self.item.link('invalid') mock_document = Mock() mock_document.parent = 'fake' self.item.document = mock_document self.assertFalse(self.item.validate()) def test_validate_tree(self): """Verify an item can be checked against a tree.""" def mock_iter(self): # pylint: disable=W0613 """Mock Tree.__iter__ to yield a mock Document.""" mock_document = Mock() mock_document.parent = 'RQ' def mock_iter2(self): # pylint: disable=W0613 """Mock Document.__iter__ to yield a mock Item.""" mock_item = Mock() mock_item.uid = 'TST001' mock_item.links = ['RQ001'] yield mock_item mock_document.__iter__ = mock_iter2 yield mock_document self.item.link('fake1') mock_tree = Mock() mock_tree.__iter__ = mock_iter mock_tree.find_item = lambda uid: Mock(uid='fake1') self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_tree_error(self): """Verify an item can be checked against a tree with errors.""" self.item.link('fake1') mock_tree = MagicMock() mock_tree.find_item = Mock(side_effect=DoorstopError) self.item.tree = mock_tree self.assertFalse(self.item.validate()) def test_validate_both(self): """Verify an item can be checked against both.""" def mock_iter(seq): """Creates a mock __iter__ method.""" def _iter(self): # pylint: disable=W0613 """Mock __iter__method.""" yield from seq return _iter mock_item = Mock() mock_item.links = [self.item.uid] mock_document = Mock() mock_document.parent = 'BOTH' mock_document.prefix = 'BOTH' mock_document.__iter__ = mock_iter([mock_item]) mock_tree = Mock() mock_tree.__iter__ = mock_iter([mock_document]) self.item.document = mock_document self.item.tree = mock_tree self.assertTrue(self.item.validate()) def test_validate_both_no_reverse_links(self): """Verify an item can be checked against both (no reverse links).""" def mock_iter(self): # pylint: disable=W0613 """Mock Tree.__iter__ to yield a mock Document.""" mock_document = Mock() mock_document.parent = 'RQ' def mock_iter2(self): # pylint: disable=W0613 """Mock Document.__iter__ to yield a mock Item.""" mock_item = Mock() mock_item.uid = 'TST001' mock_item.links = [] yield mock_item mock_document.__iter__ = mock_iter2 yield mock_document self.item.link('fake1') mock_document = Mock() mock_document.prefix = 'RQ' mock_tree = Mock() mock_tree.__iter__ = mock_iter mock_tree.find_item = lambda uid: Mock(uid='fake1') self.item.document = mock_document self.item.tree = mock_tree self.assertTrue(self.item.validate()) @patch('doorstop.core.item.Item.get_issues', Mock(return_value=[])) def test_issues(self): """Verify an item's issues convenience property can be accessed.""" self.assertEqual(0, len(self.item.issues)) def test_stamp(self): """Verify an item's contents can be stamped.""" stamp = 'c6a87755b8756b61731c704c6a7be4a2' self.assertEqual(stamp, self.item.stamp()) def test_stamp_links(self): """Verify an item's contents can be stamped.""" self.item.link('mock_link') stamp = '1020719292bbdc4090bd236cf41cd104' self.assertEqual(stamp, self.item.stamp(links=True)) def test_clear(self): """Verify an item's links can be cleared as suspect.""" mock_item = Mock() mock_item.uid = 'mock_uid' mock_item.stamp = Mock(return_value=Stamp('abc123')) mock_tree = MagicMock() mock_tree.find_item = Mock(return_value=mock_item) self.item.tree = mock_tree self.item.link('mock_uid') self.assertFalse(self.item.cleared) self.assertEqual(None, self.item.links[0].stamp) # pylint: disable=E1101 # Act self.item.clear() # Assert self.assertTrue(self.item.cleared) self.assertEqual('abc123', self.item.links[0].stamp) # pylint: disable=E1101 def test_review(self): """Verify an item can be marked as reviewed.""" self.item.reviewed = False self.item.review() self.assertTrue(self.item.reviewed) @patch('doorstop.common.delete') def test_delete(self, mock_delete): """Verify an item can be deleted.""" self.item.delete() mock_delete.assert_called_once_with(self.item.path) self.item.delete() # ensure a second delete is ignored @patch('doorstop.common.delete', Mock()) def test_delete_cache(self): """Verify an item is expunged after delete.""" self.item.tree = Mock() self.item.tree._item_cache = {self.item.uid: self.item} self.item.delete() self.assertIs(None, self.item.tree._item_cache[self.item.uid])