I'm doing (or trying to do) TDD to develop my business logic for an application.
I have a collection in one class that I expose as IReadOnlyList
, because I want to make it clear that adding/removing from the list is prohibited. Instead I expose AddItem
and RemoveItem
methods, so that I can execute some other code when an item is added or removed. See example below.
public interface IBusinessItem { }
public interface IBusinessClass
{
IReadOnlyList<IBusinessItem> Items { get; }
void AddItem(IBusinessItem item);
void RemoveItem(IBusinessItem item);
}
My problem arises when I try to follow the principle that my unit tests should fail for one reason only. Unit testing my RemoveItem
requires that I make use of AddItem
in my test arrange phase. Therefore, my test can fail either because the RemoveItem
does not remove an item as expected, or because the AddItem
didn't work as expected so RemoveItem
throws:
[Fact]
public void RemoveItemShouldRemoveItemFromItems()
{
// Arrange
var item = new MyBusinessItem();
sut.AddItem(item);
// Act
sut.RemoveItem(item); // throws exception if sut.Items is empty
// Assert
Assert.Empty(sut.Items);
}
I'm thinking that my unit test would be less fragile if I could access sut.Items.Add(item)
, instead of relying on sut.AddItem(item)
, but I don't want to change my public interface just for testing purposes.
I think this answer partially adresses my concerns. My interpretation of the answer (and one of Mark's comments to the answer) is that it is fine to invoke other methods in the arrange phase. But I feel like this could end up with a lot of tests that fails for other reasons than what they are actually testing.
My question here is: When doing TDD, is it acceptable to access the underlying List.Add
(that is not part of my public interface) in order avoid dependency of the AddItem
implementation in my RemoveItem
tests?.
A few ways I can think of to access the underlying List.Add
:
- Casting it in the test
- Changing the underlying private field to protected and create a mock that inherits from my BusinessClass and exposes that field
- Changing the underlying private field to internal and use the
InternalsVisibleTo
attribute